@heyamiko/amiko-cli 0.9.4 → 0.9.6-beta.0

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 CHANGED
@@ -138,7 +138,7 @@ amiko wallets swap send 0.5 USDC AMIKO --yes
138
138
  amiko wallets swap send 100 AMIKO SOL --slippage 100
139
139
  ```
140
140
 
141
- Swaps execute via Jupiter on Solana. Supports SOL, USDC, USDT, AMIKO, PYUSD, BONK, JUP, RAY, JitoSOL, mSOL, or any mint address. `swap send` is hard-gated: in a non-interactive shell you must pass `--yes` after quoting the cost to the user (`Solana gas + 10bps platform fee`).
141
+ Swaps execute via Jupiter on Solana. Supports SOL, USDC, USDT, AMIKO, PYUSD, BONK, JUP, RAY, JitoSOL, mSOL, or any mint address. `swap send` is hard-gated: in a non-interactive shell you must pass `--yes` after quoting the cost to the user (`Solana gas + Jupiter referral fee`).
142
142
 
143
143
  ## Bridge
144
144
 
@@ -197,7 +197,7 @@ Every paid / value-moving command (`image`, `search`, `amazon search`, `service
197
197
  amiko markets image "a sunset over mountains" --yes
198
198
  ```
199
199
 
200
- This makes cost confirmation effectively mandatory regardless of which model drives the agent — weak models can no longer silently burn tokens by ignoring the skill docs. The same refusal fires for `swap send` (Solana gas + 10 bps platform fee) and `bridge send` (Across fee + gas), so an agent cannot move tokens without a round-trip through user approval.
200
+ This makes cost confirmation effectively mandatory regardless of which model drives the agent — weak models can no longer silently burn tokens by ignoring the skill docs. The same refusal fires for `swap send` (Solana gas + Jupiter referral fee) and `bridge send` (Across fee + gas), so an agent cannot move tokens without a round-trip through user approval.
201
201
 
202
202
  ## Error Output
203
203
 
package/dist/index.js CHANGED
@@ -17305,7 +17305,8 @@ function requireAuth() {
17305
17305
  var DEFAULTS = {
17306
17306
  apiBase: "https://mpp.amikomarkets.com",
17307
17307
  platformUrl: "https://platform.heyamiko.com",
17308
- siteBase: "https://platform.heyamiko.com"
17308
+ siteBase: "https://platform.heyamiko.com",
17309
+ chatBase: "https://amiko-chat.up.railway.app"
17309
17310
  };
17310
17311
  function loadConfig() {
17311
17312
  const auth = resolveAuth();
@@ -17313,6 +17314,7 @@ function loadConfig() {
17313
17314
  apiBase: auth.apiBase ?? DEFAULTS.apiBase,
17314
17315
  platformUrl: auth.platformUrl ?? DEFAULTS.platformUrl,
17315
17316
  siteBase: DEFAULTS.siteBase,
17317
+ chatBase: DEFAULTS.chatBase,
17316
17318
  twinToken: auth.twinToken,
17317
17319
  twinId: auth.twinId,
17318
17320
  userId: auth.userId
@@ -19536,7 +19538,7 @@ function registerSwapCommand(program2) {
19536
19538
  console.log(label("Wallet", truncAddr(wallet)));
19537
19539
  console.log("");
19538
19540
  await requireApproval({
19539
- cost: "Solana gas + 10bps platform fee",
19541
+ cost: "Solana gas + Jupiter referral fee",
19540
19542
  summary: `swap ${amountStr} ${fromToken.toUpperCase()} → ~${(Number(quote.outAmount) / 10 ** output.decimals).toFixed(6)} ${output.symbol}`,
19541
19543
  yes: opts.yes,
19542
19544
  commandExample: `amiko wallets swap send ${amountStr} ${fromToken} ${toToken}`
@@ -21762,6 +21764,198 @@ function registerComposioCommand(program2) {
21762
21764
  });
21763
21765
  }
21764
21766
 
21767
+ // src/commands/conversation.ts
21768
+ function describeConversation(c, selfUserId) {
21769
+ if (c.title)
21770
+ return c.title;
21771
+ const others = c.participants?.filter((p) => !(p.participant_type === "user" && p.participant_id === selfUserId)) ?? [];
21772
+ return others.map((p) => p.name ?? p.participant_id).join(", ") || c.id;
21773
+ }
21774
+ function findDirectWithUser(list, selfUserId, otherUserId) {
21775
+ return list.find((c) => {
21776
+ if (c.conversation_type !== "direct")
21777
+ return false;
21778
+ const ps = c.participants ?? [];
21779
+ const userIds = ps.filter((p) => p.participant_type === "user" && !p.left_at).map((p) => p.participant_id);
21780
+ if (userIds.length !== 2)
21781
+ return false;
21782
+ if (selfUserId && !userIds.includes(selfUserId))
21783
+ return false;
21784
+ return userIds.includes(otherUserId);
21785
+ });
21786
+ }
21787
+ function registerConversationCommand(program2) {
21788
+ program2.command("list").description("List your conversations").option("--archived <state>", "active | archived | all", "active").option("--limit <n>", "Max results (1–100)", "50").option("--offset <n>", "Pagination offset", "0").option("--json", "Output as JSON").action(async (opts) => {
21789
+ const auth = resolveAuth();
21790
+ const config2 = loadConfig();
21791
+ const spinner = opts.json ? null : ora("Loading conversations...").start();
21792
+ try {
21793
+ const data = await amikoWebFetch(auth, "/api/conversations", {
21794
+ query: {
21795
+ archived: opts.archived,
21796
+ limit: opts.limit,
21797
+ offset: opts.offset
21798
+ }
21799
+ });
21800
+ spinner?.stop();
21801
+ renderOutput(data, (d) => {
21802
+ if (!d.conversations.length) {
21803
+ console.log(dim("No conversations."));
21804
+ return;
21805
+ }
21806
+ console.log(heading(`Conversations — ${d.conversations.length} of ${d.pagination.total}`));
21807
+ for (const c of d.conversations) {
21808
+ console.log("");
21809
+ console.log(label("ID", c.id));
21810
+ console.log(label("With", describeConversation(c, config2.userId)));
21811
+ console.log(label("Type", c.conversation_type));
21812
+ if (c.last_activity_at)
21813
+ console.log(label("Last activity", c.last_activity_at));
21814
+ if (c.unread_count)
21815
+ console.log(label("Unread", String(c.unread_count)));
21816
+ }
21817
+ if (d.pagination.has_more) {
21818
+ console.log("");
21819
+ console.log(dim(`More available — pass --offset ${Number(opts.offset ?? 0) + d.conversations.length}`));
21820
+ }
21821
+ }, { json: opts.json });
21822
+ } catch (e) {
21823
+ spinner?.stop();
21824
+ console.error(error(e instanceof Error ? e.message : String(e)));
21825
+ process.exit(1);
21826
+ }
21827
+ });
21828
+ program2.command("find").description("Find an existing direct conversation with a user").requiredOption("--user-id <id>", "Internal user id of the other party").option("--json", "Output as JSON").action(async (opts) => {
21829
+ const auth = resolveAuth();
21830
+ const config2 = loadConfig();
21831
+ const spinner = opts.json ? null : ora("Searching...").start();
21832
+ try {
21833
+ const data = await amikoWebFetch(auth, "/api/conversations", { query: { archived: "all", limit: 100 } });
21834
+ spinner?.stop();
21835
+ const match = findDirectWithUser(data.conversations, config2.userId, opts.userId);
21836
+ if (opts.json) {
21837
+ console.log(JSON.stringify({ conversation: match ?? null }, null, 2));
21838
+ return;
21839
+ }
21840
+ if (!match) {
21841
+ console.log(dim(`No direct conversation with user ${opts.userId}. Run \`amiko conversation create --user-id ${opts.userId}\` to start one.`));
21842
+ return;
21843
+ }
21844
+ console.log(success("Found conversation"));
21845
+ console.log(label("ID", match.id));
21846
+ console.log(label("With", describeConversation(match, config2.userId)));
21847
+ if (match.last_activity_at)
21848
+ console.log(label("Last activity", match.last_activity_at));
21849
+ } catch (e) {
21850
+ spinner?.stop();
21851
+ console.error(error(e instanceof Error ? e.message : String(e)));
21852
+ process.exit(1);
21853
+ }
21854
+ });
21855
+ program2.command("create").description("Create (or reuse) a direct conversation with another user").requiredOption("--user-id <id>", "Internal user id of the other party").option("--json", "Output as JSON").action(async (opts) => {
21856
+ const auth = resolveAuth();
21857
+ const spinner = opts.json ? null : ora("Creating conversation...").start();
21858
+ try {
21859
+ const data = await amikoWebFetch(auth, "/api/conversations", {
21860
+ method: "POST",
21861
+ body: {
21862
+ participant_ids: [opts.userId],
21863
+ participant_types: ["user"],
21864
+ conversation_type: "direct"
21865
+ }
21866
+ });
21867
+ spinner?.stop();
21868
+ if (opts.json) {
21869
+ console.log(JSON.stringify(data, null, 2));
21870
+ return;
21871
+ }
21872
+ console.log(success(data.reused_existing ? "Reused existing conversation" : "Conversation created"));
21873
+ console.log(label("ID", data.conversation.id));
21874
+ } catch (e) {
21875
+ spinner?.stop();
21876
+ console.error(error(e instanceof Error ? e.message : String(e)));
21877
+ process.exit(1);
21878
+ }
21879
+ });
21880
+ program2.command("read").description("Read messages from a conversation (oldest-first, paginated)").requiredOption("--id <conversationId>", "Target conversation id").option("--limit <n>", "Max messages (1–100)", "50").option("--before-cursor <cursor>", "Page back via next_cursor from a prior call").option("--json", "Output as JSON").action(async (opts) => {
21881
+ const auth = resolveAuth();
21882
+ const spinner = opts.json ? null : ora("Loading messages...").start();
21883
+ try {
21884
+ const data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(opts.id)}/messages`, {
21885
+ query: {
21886
+ limit: opts.limit,
21887
+ before_cursor: opts.beforeCursor
21888
+ }
21889
+ });
21890
+ spinner?.stop();
21891
+ if (opts.json) {
21892
+ console.log(JSON.stringify(data, null, 2));
21893
+ return;
21894
+ }
21895
+ if (!data.messages.length) {
21896
+ console.log(dim("No messages."));
21897
+ return;
21898
+ }
21899
+ console.log(heading(`Messages — ${data.messages.length}`));
21900
+ for (const m of data.messages) {
21901
+ console.log("");
21902
+ console.log(label(m.name ?? m.sender_id ?? "?", m.created_at?.slice(0, 19) ?? ""));
21903
+ if (m.content)
21904
+ console.log(m.content);
21905
+ if (m.message_type && m.message_type !== "text")
21906
+ console.log(dim(`[${m.message_type}]`));
21907
+ console.log(dim(`id: ${m.id}`));
21908
+ }
21909
+ if (data.has_more && data.next_cursor) {
21910
+ console.log("");
21911
+ console.log(dim(`Older messages available — pass --before-cursor ${data.next_cursor}`));
21912
+ }
21913
+ } catch (e) {
21914
+ spinner?.stop();
21915
+ console.error(error(e instanceof Error ? e.message : String(e)));
21916
+ process.exit(1);
21917
+ }
21918
+ });
21919
+ program2.command("send").description("Send a message into a conversation").requiredOption("--id <conversationId>", "Target conversation id").requiredOption("--message <text>", "Message body").option("--reply-to <messageId>", "Reply to a specific message").option("--json", "Output as JSON").action(async (opts) => {
21920
+ const auth = requireAuth();
21921
+ const config2 = loadConfig();
21922
+ const spinner = opts.json ? null : ora("Sending message...").start();
21923
+ try {
21924
+ const url2 = `${config2.chatBase.replace(/\/$/, "")}/messages`;
21925
+ const res = await fetch(url2, {
21926
+ method: "POST",
21927
+ headers: {
21928
+ Authorization: `Bearer ${auth.twinToken}`,
21929
+ "Content-Type": "application/json"
21930
+ },
21931
+ body: JSON.stringify({
21932
+ conversationId: opts.id,
21933
+ content: opts.message,
21934
+ message_type: "text",
21935
+ ...opts.replyTo ? { reply_to_id: opts.replyTo } : {}
21936
+ })
21937
+ });
21938
+ spinner?.stop();
21939
+ if (!res.ok) {
21940
+ const text = await res.text().catch(() => "");
21941
+ throw new Error(`${res.status} POST /messages: ${text.slice(0, 300)}`);
21942
+ }
21943
+ const data = await res.json();
21944
+ if (opts.json) {
21945
+ console.log(JSON.stringify(data, null, 2));
21946
+ return;
21947
+ }
21948
+ console.log(success("Message sent"));
21949
+ if (data.message?.id)
21950
+ console.log(label("Message ID", data.message.id));
21951
+ } catch (e) {
21952
+ spinner?.stop();
21953
+ console.error(error(e instanceof Error ? e.message : String(e)));
21954
+ process.exit(1);
21955
+ }
21956
+ });
21957
+ }
21958
+
21765
21959
  // src/commands/wallets.ts
21766
21960
  function formatBalance(b) {
21767
21961
  const n = Number(b.amount) / 10 ** b.decimals;
@@ -21976,6 +22170,7 @@ var users = program2.command("users").description("Search users and view public
21976
22170
  var post = program2.command("post").description("Create posts and comments on the feed");
21977
22171
  var review = program2.command("review").description("Review queue — list, approve, or reject twin-drafted comments");
21978
22172
  var composio = program2.command("composio").description("Manage Composio app connections");
22173
+ var conversation = program2.command("conversation").description("Conversations — list, find, create, send messages between users/agents");
21979
22174
  registerCreditsCommand(program2);
21980
22175
  registerSearchCommand(markets);
21981
22176
  registerImageCommand(markets);
@@ -22001,4 +22196,5 @@ registerFeedCommand(program2);
22001
22196
  registerPostCommand(post);
22002
22197
  registerReviewCommand(review);
22003
22198
  registerComposioCommand(composio);
22199
+ registerConversationCommand(conversation);
22004
22200
  program2.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyamiko/amiko-cli",
3
- "version": "0.9.4",
3
+ "version": "0.9.6-beta.0",
4
4
  "description": "Amiko CLI — swap tokens, manage credits, bridge cross-chain, and call marketplace agents",
5
5
  "type": "module",
6
6
  "bin": {
package/skills/SKILL.md CHANGED
@@ -32,6 +32,7 @@ Each row is a first-level command group or command. Start here when the user ask
32
32
  | `amiko review <cmd>` | Review queue — list, approve, or reject twin-drafted comments |
33
33
  | `amiko feed` | Read the friends feed, for-you feed, or filter by hashtag |
34
34
  | `amiko composio <cmd>` | Connect / disconnect third-party OAuth apps (Gmail, GitHub, …) |
35
+ | `amiko conversation <cmd>` | DM/chat — list, find, create, send messages |
35
36
  | `amiko accounts` | Show the resolved identity (authenticated, userId, twinId, platform) |
36
37
  | `amiko info` | Show the active twin (name, description, public, voice, avatar) |
37
38
  | `amiko config <cmd>` | Show resolved config |
@@ -260,6 +261,25 @@ amiko review reject <commentId> --yes # destructive — delete a draft
260
261
 
261
262
  **Comments authored by a twin always go through review.** Any comment created with `amiko post comment ... --twin <id>` (or auto-drafted by a twin in engagement mode) lands in `status=draft` and is **not visible** until you run `amiko review approve <commentId>`. Comments without `--twin` (you posting as yourself) publish immediately and skip the review queue. Workflow: `amiko review list` to see pending drafts → `amiko review approve <id>` to publish, or `amiko review reject <id> --yes` to discard.
262
263
 
264
+ ## Conversations
265
+
266
+ ```bash
267
+ amiko conversation list # all your conversations (default: active)
268
+ amiko conversation list --archived all --limit 100 # include archived
269
+ amiko conversation find --user-id <userId> # is there an existing direct DM with this user?
270
+ amiko conversation create --user-id <userId> # create or reuse a direct DM (idempotent)
271
+ amiko conversation read --id <conversationId> # read recent messages (oldest-first)
272
+ amiko conversation read --id <id> --before-cursor <next_cursor> # page older messages
273
+ amiko conversation send --id <conversationId> --message "hi" # send a text message
274
+ amiko conversation send --id <id> --message "ok" --reply-to <messageId>
275
+ ```
276
+
277
+ - `list` and `find` query amiko-web's `/api/conversations` (Prisma-backed). `find` filters client-side for a 2-user direct conversation containing both you and `--user-id`.
278
+ - `create` POSTs to `/api/conversations` with `conversation_type=direct`. The server reuses an existing conversation if one already exists between the two users (`reused_existing: true` in the JSON response), so calling it repeatedly is safe.
279
+ - `read` queries amiko-web's `GET /api/conversations/<id>/messages` (paginated by `next_cursor`, oldest-first per page).
280
+ - `send` posts to amiko-chat's HTTP fallback (`POST /messages`) with the same Privy JWT — works for plain text messages outside the WebSocket session.
281
+ - `--user-id` expects the **internal** user id (the one returned by `amiko users search`), not a Privy DID or handle.
282
+
263
283
  ## Composio
264
284
 
265
285
  ```bash