@heyamiko/amiko-cli 0.14.0-beta.8 → 0.14.1
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 +153 -18
- package/dist/index.js +2121 -304
- package/package.json +1 -1
- package/skills/SKILL.md +118 -15
package/dist/index.js
CHANGED
|
@@ -25861,27 +25861,50 @@ function readFromFile(path) {
|
|
|
25861
25861
|
twinId: asString(parsed.amikoTwinId) ?? asString(parsed.AMIKO_TWIN_ID),
|
|
25862
25862
|
userId: asString(parsed.amikoUserId) ?? asString(parsed.AMIKO_USER_ID),
|
|
25863
25863
|
platformUrl: asString(parsed.amikoPlatformUrl) ?? asString(parsed.AMIKO_PLATFORM_URL),
|
|
25864
|
-
apiBase: asString(parsed.amikoApiBase) ?? asString(parsed.AMIKO_API_BASE) ?? asString(parsed.apiBase)
|
|
25864
|
+
apiBase: asString(parsed.amikoApiBase) ?? asString(parsed.AMIKO_API_BASE) ?? asString(parsed.apiBase),
|
|
25865
|
+
billingApiKey: asString(parsed.amikoApiKey) ?? asString(parsed.AMIKO_API_KEY) ?? asString(parsed.billingApiKey),
|
|
25866
|
+
billingUrl: asString(parsed.amikoBillingUrl) ?? asString(parsed.AMIKO_BILLING_URL) ?? asString(parsed.billingUrl)
|
|
25865
25867
|
};
|
|
25866
25868
|
} catch {
|
|
25867
25869
|
return null;
|
|
25868
25870
|
}
|
|
25869
25871
|
}
|
|
25872
|
+
function readRoutingIdsFromEnv() {
|
|
25873
|
+
return {
|
|
25874
|
+
agentId: asString(process.env.OPENHERMIT_AGENT_ID),
|
|
25875
|
+
sessionId: asString(process.env.OPENHERMIT_SESSION_ID)
|
|
25876
|
+
};
|
|
25877
|
+
}
|
|
25870
25878
|
function readFromEnv() {
|
|
25871
25879
|
return {
|
|
25872
25880
|
twinToken: asString(process.env.AMIKO_TWIN_TOKEN) ?? asString(process.env.AMIKO_USER_TOKEN),
|
|
25873
25881
|
twinId: asString(process.env.AMIKO_TWIN_ID),
|
|
25874
25882
|
userId: asString(process.env.AMIKO_USER_ID),
|
|
25875
25883
|
platformUrl: asString(process.env.AMIKO_PLATFORM_URL),
|
|
25876
|
-
apiBase: asString(process.env.AMIKO_API_BASE)
|
|
25884
|
+
apiBase: asString(process.env.AMIKO_API_BASE),
|
|
25885
|
+
billingApiKey: asString(process.env.AMIKO_API_KEY),
|
|
25886
|
+
billingUrl: asString(process.env.AMIKO_BILLING_URL),
|
|
25887
|
+
...readRoutingIdsFromEnv()
|
|
25888
|
+
};
|
|
25889
|
+
}
|
|
25890
|
+
function resolveFileAuth(path) {
|
|
25891
|
+
const fromFile = readFromFile(path);
|
|
25892
|
+
if (!fromFile)
|
|
25893
|
+
return null;
|
|
25894
|
+
const fromEnv = readFromEnv();
|
|
25895
|
+
return {
|
|
25896
|
+
...fromFile,
|
|
25897
|
+
...readRoutingIdsFromEnv(),
|
|
25898
|
+
billingApiKey: fromEnv.billingApiKey ?? fromFile.billingApiKey,
|
|
25899
|
+
billingUrl: fromEnv.billingUrl ?? fromFile.billingUrl
|
|
25877
25900
|
};
|
|
25878
25901
|
}
|
|
25879
25902
|
function resolveAuth() {
|
|
25880
25903
|
const filePath = findAmikoJsonPath();
|
|
25881
25904
|
if (filePath) {
|
|
25882
|
-
const
|
|
25883
|
-
if (
|
|
25884
|
-
return
|
|
25905
|
+
const merged = resolveFileAuth(filePath);
|
|
25906
|
+
if (merged)
|
|
25907
|
+
return merged;
|
|
25885
25908
|
}
|
|
25886
25909
|
return readFromEnv();
|
|
25887
25910
|
}
|
|
@@ -27611,7 +27634,31 @@ function registerBridgeCommand(program2) {
|
|
|
27611
27634
|
import readline2 from "node:readline";
|
|
27612
27635
|
|
|
27613
27636
|
// src/lib/balance.ts
|
|
27614
|
-
|
|
27637
|
+
var DEFAULT_BILLING_URL = "https://billing.heyamiko.com";
|
|
27638
|
+
function billingBase(auth) {
|
|
27639
|
+
const url = auth.billingUrl || DEFAULT_BILLING_URL;
|
|
27640
|
+
return url.replace(/\/$/, "");
|
|
27641
|
+
}
|
|
27642
|
+
async function fetchCreditsBalanceViaAmk(auth) {
|
|
27643
|
+
const apiKey = auth.billingApiKey;
|
|
27644
|
+
if (!apiKey || !apiKey.startsWith("amk_"))
|
|
27645
|
+
return null;
|
|
27646
|
+
try {
|
|
27647
|
+
const res = await fetch(`${billingBase(auth)}/api/credits`, {
|
|
27648
|
+
method: "GET",
|
|
27649
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
27650
|
+
signal: AbortSignal.timeout(15000)
|
|
27651
|
+
});
|
|
27652
|
+
if (!res.ok)
|
|
27653
|
+
return null;
|
|
27654
|
+
const data = await res.json();
|
|
27655
|
+
const raw = data.displayBalance;
|
|
27656
|
+
return typeof raw === "number" && isFinite(raw) ? Math.round(raw) : null;
|
|
27657
|
+
} catch {
|
|
27658
|
+
return null;
|
|
27659
|
+
}
|
|
27660
|
+
}
|
|
27661
|
+
async function fetchCreditsBalanceViaPlatformProxy(auth) {
|
|
27615
27662
|
if (!auth.twinToken)
|
|
27616
27663
|
return null;
|
|
27617
27664
|
try {
|
|
@@ -27622,6 +27669,12 @@ async function fetchCreditsBalance(auth) {
|
|
|
27622
27669
|
return null;
|
|
27623
27670
|
}
|
|
27624
27671
|
}
|
|
27672
|
+
async function fetchCreditsBalance(auth) {
|
|
27673
|
+
const viaAmk = await fetchCreditsBalanceViaAmk(auth);
|
|
27674
|
+
if (viaAmk != null)
|
|
27675
|
+
return viaAmk;
|
|
27676
|
+
return fetchCreditsBalanceViaPlatformProxy(auth);
|
|
27677
|
+
}
|
|
27625
27678
|
async function printBalanceRemaining(auth) {
|
|
27626
27679
|
const balance = await fetchCreditsBalance(auth);
|
|
27627
27680
|
if (balance == null) {
|
|
@@ -28473,7 +28526,7 @@ function computeImageUsd(model, quality, size3, n) {
|
|
|
28473
28526
|
return Number((pricing[q][tier] * clamped * IMAGE_MARKUP).toFixed(6));
|
|
28474
28527
|
}
|
|
28475
28528
|
function registerImageCommand(program2) {
|
|
28476
|
-
program2.command("image <prompt>").alias("img").description("Generate an image —
|
|
28529
|
+
program2.command("image <prompt>").alias("img").description("Generate an image — pay in AMIKO, USDC, or USDT (cost varies by model/quality/size; exact amount quoted before charging)").option("--size <size>", "Image size: 1024x1024, 1024x1536, 1536x1024", "1024x1024").option("--quality <q>", "Quality: low, medium, high, auto", "high").option("--model <model>", `Model: ${SUPPORTED_MODELS.join(", ")}`, DEFAULT_IMAGE_MODEL).option("--n <count>", "Number of images (1–4)", "1").option("--background <bg>", "Background: transparent, opaque, auto").option("--format <fmt>", "Output: png, webp, jpeg", "png").option("--wallet <address>", "Wallet to pay from (defaults to your Solana wallet — run `amiko wallets create --chain solana` if you don't have one)").option("--token <token>", "Payment token (AMIKO, USDC, USDT)", "AMIKO").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
|
|
28477
28530
|
if (!SUPPORTED_MODELS.includes(opts.model)) {
|
|
28478
28531
|
console.error(error(`Unknown model "${opts.model}". Supported: ${SUPPORTED_MODELS.join(", ")}.`));
|
|
28479
28532
|
process.exit(1);
|
|
@@ -28637,6 +28690,7 @@ function registerImageCommand(program2) {
|
|
|
28637
28690
|
}
|
|
28638
28691
|
|
|
28639
28692
|
// src/commands/create.ts
|
|
28693
|
+
import { randomUUID } from "node:crypto";
|
|
28640
28694
|
var ETA = {
|
|
28641
28695
|
IMAGE: "~15–60s",
|
|
28642
28696
|
VIDEO: "~2–9 min",
|
|
@@ -28647,6 +28701,13 @@ var ETA = {
|
|
|
28647
28701
|
function statusOf(err) {
|
|
28648
28702
|
return err?.status;
|
|
28649
28703
|
}
|
|
28704
|
+
function autoApproveLimit() {
|
|
28705
|
+
const raw = process.env.AMIKO_AUTO_APPROVE_LIMIT;
|
|
28706
|
+
if (typeof raw !== "string" || raw.trim() === "")
|
|
28707
|
+
return;
|
|
28708
|
+
const limit = Number(raw);
|
|
28709
|
+
return Number.isFinite(limit) ? limit : undefined;
|
|
28710
|
+
}
|
|
28650
28711
|
function collectOption(value, previous) {
|
|
28651
28712
|
return [...previous ?? [], value];
|
|
28652
28713
|
}
|
|
@@ -28656,7 +28717,7 @@ function assertMaxUrls(resourceName, urls, max) {
|
|
|
28656
28717
|
process.exit(1);
|
|
28657
28718
|
}
|
|
28658
28719
|
}
|
|
28659
|
-
function printCompleted(final, title) {
|
|
28720
|
+
function printCompleted(final, title, deliveredToThisSession = false) {
|
|
28660
28721
|
console.log(heading(`${title}
|
|
28661
28722
|
`));
|
|
28662
28723
|
if (final.prompt)
|
|
@@ -28665,6 +28726,9 @@ function printCompleted(final, title) {
|
|
|
28665
28726
|
console.log(label("URL", final.assetUrl));
|
|
28666
28727
|
if (final.assetMime)
|
|
28667
28728
|
console.log(label("Type", final.assetMime));
|
|
28729
|
+
if (deliveredToThisSession) {
|
|
28730
|
+
console.log(success("Already delivered to the user in this chat automatically. Do NOT send this file again and do NOT paste the URL — you would duplicate it."));
|
|
28731
|
+
}
|
|
28668
28732
|
console.log("");
|
|
28669
28733
|
if (final.charged) {
|
|
28670
28734
|
const amt = final.chargeAmount ?? final.costAmiko ?? "?";
|
|
@@ -28673,6 +28737,9 @@ function printCompleted(final, title) {
|
|
|
28673
28737
|
} else {
|
|
28674
28738
|
console.log(dim("Not charged (settlement pending or waived)."));
|
|
28675
28739
|
}
|
|
28740
|
+
if (final.driveDocId) {
|
|
28741
|
+
console.log(label("Saved to drive", final.driveDocId) + dim(" (amiko drive get " + final.driveDocId + ")"));
|
|
28742
|
+
}
|
|
28676
28743
|
console.log(dim("View in your media space: https://platform.heyamiko.com/media"));
|
|
28677
28744
|
}
|
|
28678
28745
|
async function submitCreateJob(opts) {
|
|
@@ -28683,7 +28750,17 @@ async function submitCreateJob(opts) {
|
|
|
28683
28750
|
console.error(error(e5 instanceof Error ? e5.message : "auth failed"));
|
|
28684
28751
|
process.exit(1);
|
|
28685
28752
|
}
|
|
28686
|
-
|
|
28753
|
+
const payMethod = (opts.pay ?? "credits").toLowerCase();
|
|
28754
|
+
if (payMethod !== "wallet" && payMethod !== "credits") {
|
|
28755
|
+
console.error(error(`Unknown --pay "${opts.pay}". Allowed: wallet, credits.`));
|
|
28756
|
+
process.exit(1);
|
|
28757
|
+
}
|
|
28758
|
+
const payCredits = payMethod === "credits";
|
|
28759
|
+
if (payCredits && opts.jobBody.preferredToken) {
|
|
28760
|
+
console.error(dim("--token is ignored with --pay credits (credits come from your Amiko account, not a wallet token)."));
|
|
28761
|
+
}
|
|
28762
|
+
let costLine = payCredits ? "reserved from your Amiko account credits, captured on success (released on failure)" : "charged from your twin wallet on success (nothing on failure)";
|
|
28763
|
+
let quotedCostAmiko;
|
|
28687
28764
|
try {
|
|
28688
28765
|
const q = await amikoWebFetch(auth, "/api/create/quote", {
|
|
28689
28766
|
method: "POST",
|
|
@@ -28691,13 +28768,32 @@ async function submitCreateJob(opts) {
|
|
|
28691
28768
|
timeoutMs: 15000
|
|
28692
28769
|
});
|
|
28693
28770
|
if (q.cost_usd != null) {
|
|
28694
|
-
costLine = `${q.cost_amiko ?? "?"} AMIKO (≈ $${q.cost_usd.toFixed(4)}) — charged only on success`;
|
|
28771
|
+
costLine = payCredits ? `≈ ${Math.ceil(q.cost_usd * 1e4)} Amiko credits (≈ $${q.cost_usd.toFixed(4)}) — reserved on submit, captured only on success` : `${q.cost_amiko ?? "?"} AMIKO (≈ $${q.cost_usd.toFixed(4)}) — charged only on success`;
|
|
28772
|
+
}
|
|
28773
|
+
if (typeof q.cost_amiko === "string" && q.cost_amiko.trim() !== "") {
|
|
28774
|
+
const n = Number(q.cost_amiko);
|
|
28775
|
+
if (Number.isFinite(n))
|
|
28776
|
+
quotedCostAmiko = n;
|
|
28695
28777
|
}
|
|
28696
28778
|
} catch {}
|
|
28779
|
+
const preferredToken = typeof opts.jobBody.preferredToken === "string" ? opts.jobBody.preferredToken.toUpperCase() : undefined;
|
|
28780
|
+
const chargeIsAmiko = !payCredits && (preferredToken === undefined || preferredToken === "AMIKO");
|
|
28781
|
+
const autoApprovable = payCredits || chargeIsAmiko;
|
|
28782
|
+
const effectiveChargeToken = preferredToken ?? "AMIKO";
|
|
28783
|
+
let yes = opts.yes;
|
|
28784
|
+
let autoApproved = false;
|
|
28785
|
+
if (!yes) {
|
|
28786
|
+
const limit = autoApproveLimit();
|
|
28787
|
+
if (limit !== undefined && quotedCostAmiko !== undefined && quotedCostAmiko > 0 && autoApprovable && quotedCostAmiko <= limit) {
|
|
28788
|
+
yes = true;
|
|
28789
|
+
autoApproved = true;
|
|
28790
|
+
console.log(dim(`Auto-approved (cost ${quotedCostAmiko} ≤ limit ${limit} AMIKO${payCredits ? ", charged as account credits" : ""})`));
|
|
28791
|
+
}
|
|
28792
|
+
}
|
|
28697
28793
|
await requireApproval({
|
|
28698
28794
|
cost: costLine,
|
|
28699
28795
|
summary: opts.summary,
|
|
28700
|
-
yes
|
|
28796
|
+
yes,
|
|
28701
28797
|
commandExample: `amiko create ${opts.mode.toLowerCase()} ...`
|
|
28702
28798
|
});
|
|
28703
28799
|
const isTTY = process.stderr.isTTY;
|
|
@@ -28707,10 +28803,22 @@ async function submitCreateJob(opts) {
|
|
|
28707
28803
|
spinner.stop();
|
|
28708
28804
|
};
|
|
28709
28805
|
let jobId;
|
|
28806
|
+
let routedAgentId;
|
|
28807
|
+
let routedSessionId;
|
|
28710
28808
|
try {
|
|
28711
28809
|
const submit = await amikoWebFetch(auth, "/api/create/jobs", {
|
|
28712
28810
|
method: "POST",
|
|
28713
|
-
body: {
|
|
28811
|
+
body: {
|
|
28812
|
+
...opts.jobBody,
|
|
28813
|
+
twinId: auth.twinId,
|
|
28814
|
+
...autoApproved && !payCredits ? { preferredToken: effectiveChargeToken, strictToken: true } : {},
|
|
28815
|
+
...payCredits ? {
|
|
28816
|
+
paymentMethod: { kind: "account_credits" },
|
|
28817
|
+
requestId: randomUUID()
|
|
28818
|
+
} : {},
|
|
28819
|
+
...auth.agentId ? { agentId: auth.agentId } : {},
|
|
28820
|
+
...auth.sessionId ? { sessionId: auth.sessionId } : {}
|
|
28821
|
+
},
|
|
28714
28822
|
timeoutMs: 30000
|
|
28715
28823
|
});
|
|
28716
28824
|
if (!submit.jobId) {
|
|
@@ -28719,35 +28827,58 @@ async function submitCreateJob(opts) {
|
|
|
28719
28827
|
process.exit(1);
|
|
28720
28828
|
}
|
|
28721
28829
|
jobId = submit.jobId;
|
|
28830
|
+
routedAgentId = submit.agentId;
|
|
28831
|
+
routedSessionId = submit.sessionId;
|
|
28722
28832
|
} catch (e5) {
|
|
28723
28833
|
stop();
|
|
28724
|
-
|
|
28834
|
+
const webErr = e5;
|
|
28835
|
+
let message = e5 instanceof Error ? e5.message : "Submit failed";
|
|
28836
|
+
let errorCode;
|
|
28837
|
+
if (typeof webErr.body === "string" && webErr.body) {
|
|
28838
|
+
try {
|
|
28839
|
+
const parsed = JSON.parse(webErr.body);
|
|
28840
|
+
if (parsed.error)
|
|
28841
|
+
message = parsed.error;
|
|
28842
|
+
if (typeof parsed.code === "string")
|
|
28843
|
+
errorCode = parsed.code;
|
|
28844
|
+
} catch {}
|
|
28845
|
+
}
|
|
28846
|
+
console.error(error(message));
|
|
28847
|
+
if (payCredits && (errorCode === "ACCOUNT_CREDITS_DISABLED" || errorCode === "ACCOUNT_NOT_UNIFIED")) {
|
|
28848
|
+
console.error(dim("Account credits are not available for this account yet — retry without --pay credits to charge the twin wallet."));
|
|
28849
|
+
}
|
|
28725
28850
|
process.exit(1);
|
|
28726
28851
|
}
|
|
28852
|
+
const sessionRouted = !!(routedAgentId && routedSessionId);
|
|
28727
28853
|
stop();
|
|
28728
28854
|
if (opts.raw) {
|
|
28729
28855
|
console.log(JSON.stringify({
|
|
28730
28856
|
jobId,
|
|
28731
28857
|
status: "PENDING",
|
|
28732
28858
|
check: `amiko create status ${jobId}`,
|
|
28733
|
-
eta: ETA[opts.mode]
|
|
28859
|
+
eta: ETA[opts.mode],
|
|
28860
|
+
...sessionRouted ? { sessionRouted: true, deliveredToSession: routedSessionId } : {}
|
|
28734
28861
|
}, null, 2));
|
|
28735
28862
|
return;
|
|
28736
28863
|
}
|
|
28737
28864
|
console.log(success(`Submitted — job ${jobId}`));
|
|
28738
28865
|
console.log(label("Status", "PENDING — generating in the background"));
|
|
28739
28866
|
console.log(label("Check result", `amiko create status ${jobId}`));
|
|
28867
|
+
if (sessionRouted) {
|
|
28868
|
+
console.log(dim("A generating-placeholder is already showing in the chat and the finished media will be delivered there automatically — do not send the file or URL yourself."));
|
|
28869
|
+
}
|
|
28740
28870
|
console.log(dim(`Usually ready in ${ETA[opts.mode]} — charged only on success. (Also: amiko create media)`));
|
|
28741
28871
|
if (opts.mode === "VIDEO") {
|
|
28742
28872
|
console.log(dim("Async video: HTTP 202 / PENDING means the job was accepted — not a failure. Poll status after ~2–9 min."));
|
|
28743
28873
|
}
|
|
28744
28874
|
}
|
|
28745
28875
|
function registerCreateCommand(create2) {
|
|
28746
|
-
create2.command("image <prompt>").description("Generate an image (Create Studio)").option("--model <model>", "Image model: nano-banana-2 (default), nano-banana[-pro|-lite] (Google), gpt-image-2/-1.5/-1/-1-mini (OpenAI), grok-imagine-image-quality (xAI), image-01 (MiniMax)", "nano-banana-2").option("--aspect <ratio>", "Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4", "1:1").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
|
|
28876
|
+
create2.command("image <prompt>").description("Generate an image (Create Studio)").option("--model <model>", "Image model: nano-banana-2 (default), nano-banana[-pro|-lite] (Google), gpt-image-2/-1.5/-1/-1-mini (OpenAI), grok-imagine-image-quality (xAI), image-01 (MiniMax)", "nano-banana-2").option("--aspect <ratio>", "Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4", "1:1").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--pay <method>", "Payment method: credits (Amiko account credits, default) or wallet (twin wallet)", "credits").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
|
|
28747
28877
|
await submitCreateJob({
|
|
28748
28878
|
mode: "IMAGE",
|
|
28749
28879
|
summary: `Create image: ${prompt.slice(0, 80)}${prompt.length > 80 ? "…" : ""}`,
|
|
28750
28880
|
raw: opts.raw,
|
|
28881
|
+
pay: opts.pay,
|
|
28751
28882
|
yes: opts.yes,
|
|
28752
28883
|
quoteBody: { mode: "IMAGE", model: opts.model, size: opts.aspect },
|
|
28753
28884
|
jobBody: {
|
|
@@ -28759,7 +28890,7 @@ function registerCreateCommand(create2) {
|
|
|
28759
28890
|
}
|
|
28760
28891
|
});
|
|
28761
28892
|
});
|
|
28762
|
-
create2.command("video <prompt>").description("Generate a video (Create Studio)").option("--model <model>", "Video model
|
|
28893
|
+
create2.command("video <prompt>").description("Generate a video (Create Studio)").option("--model <model>", "Video model: MiniMax-Hailuo-02 (default for prompt-only; T2V+I2V), MiniMax-Hailuo-2.3-Fast (default with --first-frame; I2V only), MiniMax-Hailuo-2.3 (T2V+I2V), veo-3.1[-fast|-lite]-generate-preview (Google; durations clamp to 4/6/8s), grok-imagine-video (xAI), dreamina-seedance-* (BytePlus). ONLY these — kling and other ids are not supported").option("--resolution <res>", "512P, 720P, 768P, 1080P", "768P").option("--seconds <n>", "Clip length: 6 or 10", "6").option("--aspect <ratio>", "Aspect ratio (provider-dependent)").option("--first-frame <url>", "Image-to-video: first frame URL/data URI").option("--last-frame <url>", "First-and-last-frame video: ending frame URL/data URI").option("--reference-image <url>", "Seedance multimodal: reference image URL (repeatable, max 9)", collectOption, []).option("--reference-video <url>", "Seedance multimodal: reference video URL (repeatable, max 3)", collectOption, []).option("--reference-audio <url>", "Seedance multimodal: reference audio URL (repeatable, max 3)", collectOption, []).option("--subject-reference <url>", "MiniMax S2V: subject/character reference image URL").option("--generate-audio", "Seedance: generate audio in the output video").option("--no-generate-audio", "Seedance: output silent video").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--pay <method>", "Payment method: credits (Amiko account credits, default) or wallet (twin wallet)", "credits").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
|
|
28763
28894
|
const seconds = Number(opts.seconds);
|
|
28764
28895
|
if (seconds !== 6 && seconds !== 10) {
|
|
28765
28896
|
console.error(error("--seconds must be 6 or 10."));
|
|
@@ -28772,21 +28903,24 @@ function registerCreateCommand(create2) {
|
|
|
28772
28903
|
assertMaxUrls("--reference-video", referenceVideoUrls, 3);
|
|
28773
28904
|
assertMaxUrls("--reference-audio", referenceAudioUrls, 3);
|
|
28774
28905
|
const generateAudio = opts.generateAudio;
|
|
28906
|
+
const model = opts.model ?? (opts.firstFrame ? "MiniMax-Hailuo-2.3-Fast" : "MiniMax-Hailuo-02");
|
|
28775
28907
|
await submitCreateJob({
|
|
28776
28908
|
mode: "VIDEO",
|
|
28777
28909
|
summary: `Create video: ${prompt.slice(0, 80)}${prompt.length > 80 ? "…" : ""}`,
|
|
28778
28910
|
raw: opts.raw,
|
|
28911
|
+
pay: opts.pay,
|
|
28779
28912
|
yes: opts.yes,
|
|
28780
28913
|
quoteBody: {
|
|
28781
28914
|
mode: "VIDEO",
|
|
28782
|
-
model
|
|
28915
|
+
model,
|
|
28783
28916
|
size: opts.resolution,
|
|
28784
|
-
seconds
|
|
28917
|
+
seconds,
|
|
28918
|
+
...referenceVideoUrls.length ? { referenceVideoUrls } : {}
|
|
28785
28919
|
},
|
|
28786
28920
|
jobBody: {
|
|
28787
28921
|
mode: "VIDEO",
|
|
28788
28922
|
prompt,
|
|
28789
|
-
model
|
|
28923
|
+
model,
|
|
28790
28924
|
resolution: opts.resolution,
|
|
28791
28925
|
seconds,
|
|
28792
28926
|
...opts.aspect ? { aspectRatio: opts.aspect } : {},
|
|
@@ -28808,23 +28942,59 @@ function registerCreateCommand(create2) {
|
|
|
28808
28942
|
}
|
|
28809
28943
|
});
|
|
28810
28944
|
});
|
|
28811
|
-
|
|
28945
|
+
const DEFAULT_EL_TTS_MODEL = "eleven_multilingual_v2";
|
|
28946
|
+
const DEFAULT_EL_TTS_VOICE = "21m00Tcm4TlvDq8ikWAM";
|
|
28947
|
+
const DEFAULT_MM_TTS_MODEL = "speech-2.8-turbo";
|
|
28948
|
+
const DEFAULT_MM_TTS_VOICE = "English_expressive_narrator";
|
|
28949
|
+
const MINIMAX_TTS_MODELS = new Set([
|
|
28950
|
+
"speech-2.8-turbo",
|
|
28951
|
+
"speech-2.8-hd",
|
|
28952
|
+
"speech-2.6-turbo",
|
|
28953
|
+
"speech-2.6-hd"
|
|
28954
|
+
]);
|
|
28955
|
+
create2.command("tts <text>").description("Generate speech from text (Create Studio)").option("--provider <name>", "TTS provider: minimax | elevenlabs (sets defaults when --model/--voice omitted)").option("--model <model>", `TTS model id (MiniMax: speech-2.8-turbo|hd, speech-2.6-turbo|hd; ElevenLabs: eleven_*)`).option("--voice <voiceId>", "Voice id (MiniMax system e.g. English_expressive_narrator, or a cloned voice_id; ElevenLabs voice hash)").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--pay <method>", "Payment method: credits (Amiko account credits, default) or wallet (twin wallet)", "credits").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (text, opts) => {
|
|
28956
|
+
const providerRaw = typeof opts.provider === "string" ? opts.provider.trim().toLowerCase() : "";
|
|
28957
|
+
if (providerRaw && providerRaw !== "minimax" && providerRaw !== "elevenlabs") {
|
|
28958
|
+
console.error(error("--provider must be minimax or elevenlabs"));
|
|
28959
|
+
process.exit(1);
|
|
28960
|
+
}
|
|
28961
|
+
let model = typeof opts.model === "string" && opts.model.trim().length > 0 ? opts.model.trim() : undefined;
|
|
28962
|
+
let voice = typeof opts.voice === "string" && opts.voice.trim().length > 0 ? opts.voice.trim() : undefined;
|
|
28963
|
+
let provider = providerRaw === "minimax" || providerRaw === "elevenlabs" ? providerRaw : model && MINIMAX_TTS_MODELS.has(model) ? "minimax" : model && model.startsWith("eleven") ? "elevenlabs" : "elevenlabs";
|
|
28964
|
+
if (!model) {
|
|
28965
|
+
model = provider === "minimax" ? DEFAULT_MM_TTS_MODEL : DEFAULT_EL_TTS_MODEL;
|
|
28966
|
+
}
|
|
28967
|
+
if (!voice) {
|
|
28968
|
+
voice = provider === "minimax" ? DEFAULT_MM_TTS_VOICE : DEFAULT_EL_TTS_VOICE;
|
|
28969
|
+
}
|
|
28970
|
+
if (MINIMAX_TTS_MODELS.has(model) && providerRaw !== "elevenlabs") {
|
|
28971
|
+
provider = "minimax";
|
|
28972
|
+
}
|
|
28973
|
+
if (provider === "minimax" && !MINIMAX_TTS_MODELS.has(model)) {
|
|
28974
|
+
console.error(error(`--provider minimax requires a MiniMax speech model (${[...MINIMAX_TTS_MODELS].join(", ")})`));
|
|
28975
|
+
process.exit(1);
|
|
28976
|
+
}
|
|
28977
|
+
if (provider === "elevenlabs" && !model.startsWith("eleven")) {
|
|
28978
|
+
console.error(error("--provider elevenlabs requires an eleven_* model"));
|
|
28979
|
+
process.exit(1);
|
|
28980
|
+
}
|
|
28812
28981
|
await submitCreateJob({
|
|
28813
28982
|
mode: "TTS",
|
|
28814
|
-
summary: `Create speech: ${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`,
|
|
28983
|
+
summary: `Create speech (${provider}/${model}): ${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`,
|
|
28815
28984
|
raw: opts.raw,
|
|
28985
|
+
pay: opts.pay,
|
|
28816
28986
|
yes: opts.yes,
|
|
28817
|
-
quoteBody: { mode: "TTS", model
|
|
28987
|
+
quoteBody: { mode: "TTS", model, prompt: text, voiceId: voice },
|
|
28818
28988
|
jobBody: {
|
|
28819
28989
|
mode: "TTS",
|
|
28820
28990
|
prompt: text,
|
|
28821
|
-
model
|
|
28822
|
-
voiceId:
|
|
28991
|
+
model,
|
|
28992
|
+
voiceId: voice,
|
|
28823
28993
|
...opts.token ? { preferredToken: opts.token } : {}
|
|
28824
28994
|
}
|
|
28825
28995
|
});
|
|
28826
28996
|
});
|
|
28827
|
-
create2.command("music [prompt]").description("Generate music (Create Studio)").option("--model <model>", "music_v1, music-2.6, music-cover", "music-2.6").option("--lyrics <lyrics>", "Lyrics (optional)").option("--duration <ms>", "Length in ms").option("--instrumental", "Instrumental (no vocals)").option("--lyrics-optimizer", "Auto-write lyrics from the prompt (music_v1 only; on by default when no --lyrics/--instrumental)").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
|
|
28997
|
+
create2.command("music [prompt]").description("Generate music (Create Studio)").option("--model <model>", "music_v1, music-2.6[-free], music-cover[-free]", "music-2.6").option("--lyrics <lyrics>", "Lyrics (optional)").option("--duration <ms>", "Length in ms").option("--instrumental", "Instrumental (no vocals)").option("--lyrics-optimizer", "Auto-write lyrics from the prompt (music_v1 only; on by default when no --lyrics/--instrumental)").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--pay <method>", "Payment method: credits (Amiko account credits, default) or wallet (twin wallet)", "credits").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
|
|
28828
28998
|
if (!prompt && !opts.lyrics) {
|
|
28829
28999
|
console.error(error("Provide a prompt or --lyrics."));
|
|
28830
29000
|
process.exit(1);
|
|
@@ -28833,6 +29003,7 @@ function registerCreateCommand(create2) {
|
|
|
28833
29003
|
mode: "MUSIC",
|
|
28834
29004
|
summary: `Create music: ${(prompt ?? opts.lyrics ?? "").slice(0, 80)}`,
|
|
28835
29005
|
raw: opts.raw,
|
|
29006
|
+
pay: opts.pay,
|
|
28836
29007
|
yes: opts.yes,
|
|
28837
29008
|
quoteBody: { mode: "MUSIC", model: opts.model },
|
|
28838
29009
|
jobBody: {
|
|
@@ -28847,11 +29018,12 @@ function registerCreateCommand(create2) {
|
|
|
28847
29018
|
}
|
|
28848
29019
|
});
|
|
28849
29020
|
});
|
|
28850
|
-
create2.command("sfx <text>").description("Generate a sound effect (Create Studio)").option("--duration <seconds>", "Clip length in seconds").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (text, opts) => {
|
|
29021
|
+
create2.command("sfx <text>").description("Generate a sound effect (Create Studio)").option("--duration <seconds>", "Clip length in seconds").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--pay <method>", "Payment method: credits (Amiko account credits, default) or wallet (twin wallet)", "credits").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (text, opts) => {
|
|
28851
29022
|
await submitCreateJob({
|
|
28852
29023
|
mode: "SFX",
|
|
28853
29024
|
summary: `Create SFX: ${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`,
|
|
28854
29025
|
raw: opts.raw,
|
|
29026
|
+
pay: opts.pay,
|
|
28855
29027
|
yes: opts.yes,
|
|
28856
29028
|
quoteBody: { mode: "SFX" },
|
|
28857
29029
|
jobBody: {
|
|
@@ -28881,8 +29053,9 @@ function registerCreateCommand(create2) {
|
|
|
28881
29053
|
}
|
|
28882
29054
|
process.exit(1);
|
|
28883
29055
|
}
|
|
29056
|
+
const deliveredToThisSession = !!(p.agentId && p.sessionId && auth.sessionId && p.sessionId === auth.sessionId);
|
|
28884
29057
|
if (opts.raw) {
|
|
28885
|
-
console.log(JSON.stringify(p, null, 2));
|
|
29058
|
+
console.log(JSON.stringify(deliveredToThisSession ? { ...p, sessionRouted: true, deliveredToSession: p.sessionId } : p, null, 2));
|
|
28886
29059
|
return;
|
|
28887
29060
|
}
|
|
28888
29061
|
if (p.status === "FAILED") {
|
|
@@ -28894,7 +29067,7 @@ function registerCreateCommand(create2) {
|
|
|
28894
29067
|
console.log(dim(`Job id: ${jobId}`));
|
|
28895
29068
|
return;
|
|
28896
29069
|
}
|
|
28897
|
-
printCompleted(p, "Job complete");
|
|
29070
|
+
printCompleted(p, "Job complete", deliveredToThisSession);
|
|
28898
29071
|
});
|
|
28899
29072
|
create2.command("media").description("List your recent Create Studio generations").option("--limit <n>", "How many to show (max 50)", "20").option("--service <type>", "Filter by type: image, video, tts, music, sfx").option("--raw", "Output raw JSON").action(async (opts) => {
|
|
28900
29073
|
let auth;
|
|
@@ -28948,41 +29121,42 @@ import { basename, extname } from "node:path";
|
|
|
28948
29121
|
var MAX_BYTES = 100 * 1024 * 1024;
|
|
28949
29122
|
var FETCH_TIMEOUT_MS = 30000;
|
|
28950
29123
|
async function resolveFileInput(src, opts = {}) {
|
|
29124
|
+
const maxBytes = opts.maxBytes ?? MAX_BYTES;
|
|
28951
29125
|
if (src === "-" || opts.stdin) {
|
|
28952
|
-
const buffer2 = await readStdin();
|
|
29126
|
+
const buffer2 = await readStdin(maxBytes);
|
|
28953
29127
|
const name2 = opts.hintName ?? "stdin";
|
|
28954
29128
|
const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
|
|
28955
29129
|
return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
|
|
28956
29130
|
}
|
|
28957
29131
|
if (/^https?:\/\//i.test(src)) {
|
|
28958
|
-
const buffer2 = await fetchRemote(src);
|
|
29132
|
+
const buffer2 = await fetchRemote(src, maxBytes);
|
|
28959
29133
|
const urlPath = new URL(src).pathname;
|
|
28960
29134
|
const name2 = opts.hintName ?? (basename(urlPath) || "download");
|
|
28961
29135
|
const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
|
|
28962
29136
|
return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
|
|
28963
29137
|
}
|
|
28964
29138
|
const buffer = await readFile(src);
|
|
28965
|
-
if (buffer.length >
|
|
28966
|
-
throw new Error(`File too large: ${buffer.length} bytes exceeds ${
|
|
29139
|
+
if (buffer.length > maxBytes) {
|
|
29140
|
+
throw new Error(`File too large: ${buffer.length} bytes exceeds ${maxBytes} byte cap.`);
|
|
28967
29141
|
}
|
|
28968
29142
|
const name = opts.hintName ?? basename(src);
|
|
28969
29143
|
const mime = opts.hintMime ?? detectMime(buffer, name);
|
|
28970
29144
|
return { buffer, name, mime, size: buffer.length };
|
|
28971
29145
|
}
|
|
28972
|
-
async function readStdin() {
|
|
29146
|
+
async function readStdin(maxBytes) {
|
|
28973
29147
|
const chunks = [];
|
|
28974
29148
|
let total = 0;
|
|
28975
29149
|
for await (const chunk of process.stdin) {
|
|
28976
29150
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
28977
29151
|
total += buf.length;
|
|
28978
|
-
if (total >
|
|
28979
|
-
throw new Error(`stdin exceeded ${
|
|
29152
|
+
if (total > maxBytes) {
|
|
29153
|
+
throw new Error(`stdin exceeded ${maxBytes} byte cap.`);
|
|
28980
29154
|
}
|
|
28981
29155
|
chunks.push(buf);
|
|
28982
29156
|
}
|
|
28983
29157
|
return Buffer.concat(chunks);
|
|
28984
29158
|
}
|
|
28985
|
-
async function fetchRemote(url2) {
|
|
29159
|
+
async function fetchRemote(url2, maxBytes) {
|
|
28986
29160
|
const controller = new AbortController;
|
|
28987
29161
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
28988
29162
|
try {
|
|
@@ -28991,12 +29165,12 @@ async function fetchRemote(url2) {
|
|
|
28991
29165
|
throw new Error(`Fetch ${url2} failed: ${res.status} ${res.statusText}`);
|
|
28992
29166
|
}
|
|
28993
29167
|
const len = Number(res.headers.get("content-length") || 0);
|
|
28994
|
-
if (len >
|
|
28995
|
-
throw new Error(`Remote file too large: ${len} bytes exceeds ${
|
|
29168
|
+
if (len > maxBytes) {
|
|
29169
|
+
throw new Error(`Remote file too large: ${len} bytes exceeds ${maxBytes} byte cap.`);
|
|
28996
29170
|
}
|
|
28997
29171
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
28998
|
-
if (buf.length >
|
|
28999
|
-
throw new Error(`Remote file too large: ${buf.length} bytes exceeds ${
|
|
29172
|
+
if (buf.length > maxBytes) {
|
|
29173
|
+
throw new Error(`Remote file too large: ${buf.length} bytes exceeds ${maxBytes} byte cap.`);
|
|
29000
29174
|
}
|
|
29001
29175
|
return buf;
|
|
29002
29176
|
} finally {
|
|
@@ -29027,6 +29201,13 @@ var EXT_MIME = {
|
|
|
29027
29201
|
".mp3": "audio/mpeg",
|
|
29028
29202
|
".m4a": "audio/mp4",
|
|
29029
29203
|
".ogg": "audio/ogg",
|
|
29204
|
+
".aac": "audio/aac",
|
|
29205
|
+
".flac": "audio/flac",
|
|
29206
|
+
".mp4": "video/mp4",
|
|
29207
|
+
".m4v": "video/x-m4v",
|
|
29208
|
+
".webm": "video/webm",
|
|
29209
|
+
".mov": "video/quicktime",
|
|
29210
|
+
".mkv": "video/x-matroska",
|
|
29030
29211
|
".zip": "application/zip"
|
|
29031
29212
|
};
|
|
29032
29213
|
function detectMime(buffer, name) {
|
|
@@ -29220,6 +29401,170 @@ async function resolveGroup(auth, target) {
|
|
|
29220
29401
|
}
|
|
29221
29402
|
return fetchConversationById(auth, matches[0].id);
|
|
29222
29403
|
}
|
|
29404
|
+
async function fetchUserById(auth, id) {
|
|
29405
|
+
try {
|
|
29406
|
+
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(id)}`, { timeoutMs: 15000 });
|
|
29407
|
+
return prof.user?.id ? { ...prof.user, id: prof.user.id } : null;
|
|
29408
|
+
} catch (e5) {
|
|
29409
|
+
if (e5.status === 404)
|
|
29410
|
+
return null;
|
|
29411
|
+
throw e5;
|
|
29412
|
+
}
|
|
29413
|
+
}
|
|
29414
|
+
async function findDmByUserId(auth, userId) {
|
|
29415
|
+
const want = normalizeUserId(userId);
|
|
29416
|
+
const limit = 100;
|
|
29417
|
+
for (let offset = 0;offset < 1e4; offset += limit) {
|
|
29418
|
+
const data = await amikoWebFetch(auth, "/api/conversations", {
|
|
29419
|
+
query: { limit, offset },
|
|
29420
|
+
timeoutMs: 20000
|
|
29421
|
+
});
|
|
29422
|
+
const page = data.conversations ?? [];
|
|
29423
|
+
const dm = page.find((c) => c.conversation_type === "direct" && (c.participants ?? []).some((p) => p.participant_type === "user" && normalizeUserId(p.participant_id) === want));
|
|
29424
|
+
if (dm)
|
|
29425
|
+
return dm.id;
|
|
29426
|
+
if (page.length < limit || data.pagination?.has_more === false)
|
|
29427
|
+
return null;
|
|
29428
|
+
}
|
|
29429
|
+
return null;
|
|
29430
|
+
}
|
|
29431
|
+
function personLabel(u) {
|
|
29432
|
+
const name = u.name ?? u.id;
|
|
29433
|
+
return u.handle ? `${name} (@${u.handle})` : name;
|
|
29434
|
+
}
|
|
29435
|
+
async function resolveConversation(auth, target) {
|
|
29436
|
+
const t = target.trim();
|
|
29437
|
+
if (!t) {
|
|
29438
|
+
throw new Error("No target given. Pass a conversation id, a user id, an @handle, or a name.");
|
|
29439
|
+
}
|
|
29440
|
+
const person = (u) => ({
|
|
29441
|
+
kind: "user",
|
|
29442
|
+
userId: u.id,
|
|
29443
|
+
label: `DM with ${personLabel(u)}`
|
|
29444
|
+
});
|
|
29445
|
+
if (t.startsWith("@")) {
|
|
29446
|
+
const handle = t.slice(1);
|
|
29447
|
+
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
|
|
29448
|
+
timeoutMs: 15000
|
|
29449
|
+
});
|
|
29450
|
+
if (!prof.user?.id)
|
|
29451
|
+
throw new Error(`No user @${handle}.`);
|
|
29452
|
+
return person({ ...prof.user, id: prof.user.id });
|
|
29453
|
+
}
|
|
29454
|
+
const idish = normalizeUserId(t);
|
|
29455
|
+
if (looksLikeConversationId(idish)) {
|
|
29456
|
+
if (idish === t) {
|
|
29457
|
+
try {
|
|
29458
|
+
const conv = await fetchConversationById(auth, t);
|
|
29459
|
+
return {
|
|
29460
|
+
kind: "conversation",
|
|
29461
|
+
id: conv.id,
|
|
29462
|
+
label: conv.conversation_type === "direct" ? `DM with ${convLabel(conv, auth.userId)}` : `group "${convLabel(conv, auth.userId)}"`,
|
|
29463
|
+
conversation: conv
|
|
29464
|
+
};
|
|
29465
|
+
} catch (e5) {
|
|
29466
|
+
const status = e5.status;
|
|
29467
|
+
if (status === 403) {
|
|
29468
|
+
throw new Error(`Conversation ${t} exists but you are not a participant.`);
|
|
29469
|
+
}
|
|
29470
|
+
if (status !== undefined && status !== 404)
|
|
29471
|
+
throw e5;
|
|
29472
|
+
}
|
|
29473
|
+
}
|
|
29474
|
+
const user = await fetchUserById(auth, idish);
|
|
29475
|
+
if (!user) {
|
|
29476
|
+
throw new Error(`"${t}" is neither a conversation nor a user id — check \`amiko chat list\` or \`amiko friends list --json\`.`);
|
|
29477
|
+
}
|
|
29478
|
+
return person(user);
|
|
29479
|
+
}
|
|
29480
|
+
const groupId = await findGroupIdByExactTitle(auth, t);
|
|
29481
|
+
if (groupId)
|
|
29482
|
+
return { kind: "conversation", id: groupId, label: `group "${t}"` };
|
|
29483
|
+
const { resolved, failures } = await resolveMembers(auth, [t]);
|
|
29484
|
+
if (failures.length || resolved.length !== 1) {
|
|
29485
|
+
const reason = failures[0]?.reason ?? "could not be resolved";
|
|
29486
|
+
throw new Error(`"${t}" ${reason}`);
|
|
29487
|
+
}
|
|
29488
|
+
const p = resolved[0];
|
|
29489
|
+
return person({ id: p.userId, name: p.name, handle: p.handle });
|
|
29490
|
+
}
|
|
29491
|
+
|
|
29492
|
+
// src/lib/mentions.ts
|
|
29493
|
+
var MENTION_ALL_TOKEN = "@[all](all:all)";
|
|
29494
|
+
var MENTION_ALL_LITERAL_SRC = String.raw`(^|[^\w@])@all\b`;
|
|
29495
|
+
function hasMentionAllToken(content) {
|
|
29496
|
+
return content.includes(MENTION_ALL_TOKEN);
|
|
29497
|
+
}
|
|
29498
|
+
function hasMentionAllLiteral(content) {
|
|
29499
|
+
return new RegExp(MENTION_ALL_LITERAL_SRC, "i").test(content);
|
|
29500
|
+
}
|
|
29501
|
+
function rewriteMentionAllLiteral(content) {
|
|
29502
|
+
return content.replace(new RegExp(MENTION_ALL_LITERAL_SRC, "gi"), `$1${MENTION_ALL_TOKEN}`);
|
|
29503
|
+
}
|
|
29504
|
+
function applyMentionAll(content) {
|
|
29505
|
+
if (hasMentionAllToken(content))
|
|
29506
|
+
return content;
|
|
29507
|
+
return content ? `${MENTION_ALL_TOKEN} ${content}` : MENTION_ALL_TOKEN;
|
|
29508
|
+
}
|
|
29509
|
+
function mentionMarkupToPlainText(content) {
|
|
29510
|
+
return content.replace(/@\[([^\]]+)\]\(([^:()\s]+):(user|agent|all)\)/g, "@$1");
|
|
29511
|
+
}
|
|
29512
|
+
function canMentionAll(conv, selfUserId) {
|
|
29513
|
+
if (conv.conversation_type !== "group" || !selfUserId)
|
|
29514
|
+
return false;
|
|
29515
|
+
const me = normalizeUserId(selfUserId);
|
|
29516
|
+
const self = (conv.participants ?? []).find((p) => p.participant_type === "user" && normalizeUserId(p.participant_id) === me && !p.left_at);
|
|
29517
|
+
if (!self)
|
|
29518
|
+
return false;
|
|
29519
|
+
return self.role === "admin" || self.role === "owner" || conv.allow_everyone_mention_all === true;
|
|
29520
|
+
}
|
|
29521
|
+
|
|
29522
|
+
// src/lib/reactions.ts
|
|
29523
|
+
var EMOJI_MAX = 32;
|
|
29524
|
+
function validateEmoji(raw) {
|
|
29525
|
+
const emoji3 = (raw ?? "").trim();
|
|
29526
|
+
if (!emoji3)
|
|
29527
|
+
throw new Error("No emoji given. Pass the emoji to react with, e.g. ❤️.");
|
|
29528
|
+
if (emoji3.length > EMOJI_MAX) {
|
|
29529
|
+
throw new Error(`Emoji is too long (max ${EMOJI_MAX} characters).`);
|
|
29530
|
+
}
|
|
29531
|
+
return emoji3;
|
|
29532
|
+
}
|
|
29533
|
+
async function fetchMessage(auth, messageId) {
|
|
29534
|
+
const data = await amikoWebFetch(auth, `/api/messages/${encodeURIComponent(messageId)}`, { timeoutMs: 20000 });
|
|
29535
|
+
if (!data.message?.id) {
|
|
29536
|
+
throw new Error(`Message ${messageId} not found.`);
|
|
29537
|
+
}
|
|
29538
|
+
return { ...data.message, reactions: data.message.reactions ?? [] };
|
|
29539
|
+
}
|
|
29540
|
+
function ownReaction(reactions, userId) {
|
|
29541
|
+
if (!userId)
|
|
29542
|
+
return null;
|
|
29543
|
+
const me = normalizeUserId(userId);
|
|
29544
|
+
const mine = reactions.find((r) => normalizeUserId(r.user_id) === me);
|
|
29545
|
+
return mine?.emoji ?? null;
|
|
29546
|
+
}
|
|
29547
|
+
function previewText(msg, max = 60) {
|
|
29548
|
+
const content = (msg.content ?? "").trim();
|
|
29549
|
+
if (content) {
|
|
29550
|
+
return content.length > max ? `${content.slice(0, max)}…` : content;
|
|
29551
|
+
}
|
|
29552
|
+
if (msg.message_type === "voice")
|
|
29553
|
+
return "[voice message]";
|
|
29554
|
+
if (msg.metadata?.images?.length)
|
|
29555
|
+
return "[image]";
|
|
29556
|
+
return `[${msg.message_type || "message"}]`;
|
|
29557
|
+
}
|
|
29558
|
+
function groupReactions(reactions) {
|
|
29559
|
+
const counts = new Map;
|
|
29560
|
+
for (const r of reactions) {
|
|
29561
|
+
counts.set(r.emoji, (counts.get(r.emoji) ?? 0) + 1);
|
|
29562
|
+
}
|
|
29563
|
+
return Array.from(counts.entries());
|
|
29564
|
+
}
|
|
29565
|
+
function summarizeReactions(reactions) {
|
|
29566
|
+
return groupReactions(reactions).map(([emoji3, count]) => `${emoji3}×${count}`).join(" ");
|
|
29567
|
+
}
|
|
29223
29568
|
|
|
29224
29569
|
// src/commands/chat-group.ts
|
|
29225
29570
|
var collect = (v, prev) => [...prev, v];
|
|
@@ -29359,7 +29704,7 @@ function registerChatGroupCommand(group) {
|
|
|
29359
29704
|
g.id,
|
|
29360
29705
|
String(g.participants?.length ?? "-"),
|
|
29361
29706
|
g.unread_count ? String(g.unread_count) : "-",
|
|
29362
|
-
g.last_message?.content
|
|
29707
|
+
g.last_message?.content ? mentionMarkupToPlainText(g.last_message.content).slice(0, 40) : "-"
|
|
29363
29708
|
])
|
|
29364
29709
|
];
|
|
29365
29710
|
console.log(table(rows));
|
|
@@ -29376,6 +29721,7 @@ function registerChatGroupCommand(group) {
|
|
|
29376
29721
|
console.log(heading(conv.title ?? "(untitled group)"));
|
|
29377
29722
|
console.log(label("ID", conv.id));
|
|
29378
29723
|
console.log(label("Members", String(active.length)));
|
|
29724
|
+
console.log(label("@all mentions", conv.allow_everyone_mention_all ? "everyone" : "admins only"));
|
|
29379
29725
|
console.log("");
|
|
29380
29726
|
const rows = [
|
|
29381
29727
|
[dim("NAME"), dim("HANDLE"), dim("USER ID"), dim("TYPE"), dim("ROLE")],
|
|
@@ -29581,111 +29927,461 @@ function registerChatGroupCommand(group) {
|
|
|
29581
29927
|
}
|
|
29582
29928
|
console.log(success(`${memberLabel(member)} is now an admin.`));
|
|
29583
29929
|
});
|
|
29584
|
-
|
|
29585
|
-
|
|
29586
|
-
|
|
29587
|
-
|
|
29588
|
-
|
|
29589
|
-
|
|
29590
|
-
|
|
29591
|
-
|
|
29592
|
-
|
|
29593
|
-
|
|
29594
|
-
|
|
29595
|
-
|
|
29930
|
+
group.command("mention-all <group> <state>").description("Allow everyone (on) or only admins (off, the default) to use @all in a group (admins only)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, stateArg, opts) => {
|
|
29931
|
+
const state = stateArg.trim().toLowerCase();
|
|
29932
|
+
if (state !== "on" && state !== "off") {
|
|
29933
|
+
console.error(error(`Pass "on" or "off" (got "${stateArg}"): on lets every member use @all, off restricts it to admins.`));
|
|
29934
|
+
process.exit(1);
|
|
29935
|
+
}
|
|
29936
|
+
const enable = state === "on";
|
|
29937
|
+
const auth = authOrExit();
|
|
29938
|
+
const conv = await resolveGroupOrExit(auth, target);
|
|
29939
|
+
await confirmDestructive({
|
|
29940
|
+
action: enable ? `Allow EVERYONE in "${conv.title ?? conv.id}" to use @all (notifies every member)` : `Restrict @all in "${conv.title ?? conv.id}" to group admins only`,
|
|
29941
|
+
yes: opts.yes,
|
|
29942
|
+
commandExample: `amiko chat group mention-all ${JSON.stringify(target)} ${state}`
|
|
29943
|
+
});
|
|
29944
|
+
let data;
|
|
29945
|
+
try {
|
|
29946
|
+
data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(conv.id)}/mention-all-permission`, {
|
|
29947
|
+
method: "PATCH",
|
|
29948
|
+
body: { allow_everyone_mention_all: enable },
|
|
29949
|
+
timeoutMs: 20000
|
|
29950
|
+
});
|
|
29951
|
+
} catch (e5) {
|
|
29952
|
+
if (statusOf2(e5) === 403) {
|
|
29953
|
+
console.error(error(`Changing the @all setting failed. ${ADMIN_TIP}`));
|
|
29954
|
+
process.exit(1);
|
|
29955
|
+
}
|
|
29956
|
+
exitWithError(e5, "Failed to update the @all setting");
|
|
29957
|
+
}
|
|
29958
|
+
if (opts.raw) {
|
|
29959
|
+
console.log(JSON.stringify(data, null, 2));
|
|
29960
|
+
return;
|
|
29961
|
+
}
|
|
29962
|
+
const now = data.conversation?.allow_everyone_mention_all ?? enable;
|
|
29963
|
+
console.log(success(`@all in "${conv.title ?? conv.id}": ${now ? "everyone can use it" : "admins only"}`) + dim(` (${conv.id})`));
|
|
29596
29964
|
});
|
|
29597
|
-
const id = data.conversation?.id;
|
|
29598
|
-
if (!id)
|
|
29599
|
-
throw new Error("Could not open a conversation with that user.");
|
|
29600
|
-
return id;
|
|
29601
29965
|
}
|
|
29602
|
-
|
|
29966
|
+
|
|
29967
|
+
// src/lib/gifs.ts
|
|
29968
|
+
function parseUrl(url2) {
|
|
29603
29969
|
try {
|
|
29604
|
-
|
|
29605
|
-
|
|
29606
|
-
|
|
29607
|
-
if (e5.status === 404)
|
|
29608
|
-
return null;
|
|
29609
|
-
throw e5;
|
|
29970
|
+
return new URL(url2);
|
|
29971
|
+
} catch {
|
|
29972
|
+
return null;
|
|
29610
29973
|
}
|
|
29611
29974
|
}
|
|
29612
|
-
|
|
29613
|
-
const
|
|
29614
|
-
|
|
29615
|
-
|
|
29616
|
-
|
|
29617
|
-
query: { limit, offset },
|
|
29618
|
-
timeoutMs: 20000
|
|
29619
|
-
});
|
|
29620
|
-
const page = data.conversations ?? [];
|
|
29621
|
-
const dm = page.find((c) => c.conversation_type === "direct" && (c.participants ?? []).some((p) => p.participant_type === "user" && normalizeUserId(p.participant_id) === want));
|
|
29622
|
-
if (dm)
|
|
29623
|
-
return dm.id;
|
|
29624
|
-
if (page.length < limit || data.pagination?.has_more === false)
|
|
29625
|
-
return null;
|
|
29626
|
-
}
|
|
29627
|
-
return null;
|
|
29975
|
+
function isKlipyMediaUrl(url2) {
|
|
29976
|
+
const parsed = parseUrl(url2);
|
|
29977
|
+
if (!parsed)
|
|
29978
|
+
return false;
|
|
29979
|
+
return parsed.hostname === "klipy.com" || parsed.hostname.endsWith(".klipy.com");
|
|
29628
29980
|
}
|
|
29629
|
-
|
|
29630
|
-
|
|
29631
|
-
|
|
29981
|
+
var KLIPY_VIDEO_EXTENSION_RE = /\.(?:mp4|webm)(?:[?#].*)?$/i;
|
|
29982
|
+
function isKlipyVideoMediaUrl(url2) {
|
|
29983
|
+
if (!isKlipyMediaUrl(url2))
|
|
29984
|
+
return false;
|
|
29985
|
+
const parsed = parseUrl(url2);
|
|
29986
|
+
if (!parsed)
|
|
29987
|
+
return false;
|
|
29988
|
+
return KLIPY_VIDEO_EXTENSION_RE.test(parsed.pathname);
|
|
29632
29989
|
}
|
|
29633
|
-
async function
|
|
29634
|
-
|
|
29635
|
-
|
|
29636
|
-
throw new Error("No target given. Pass a conversation id, a user id, an @handle, or a name.");
|
|
29637
|
-
}
|
|
29638
|
-
const person = (u) => ({
|
|
29639
|
-
kind: "user",
|
|
29640
|
-
userId: u.id,
|
|
29641
|
-
label: `DM with ${personLabel(u)}`
|
|
29990
|
+
async function searchGifs(auth, q, page = 1) {
|
|
29991
|
+
return amikoWebFetch(auth, "/api/gifs", {
|
|
29992
|
+
query: { q, page }
|
|
29642
29993
|
});
|
|
29643
|
-
|
|
29644
|
-
|
|
29645
|
-
|
|
29646
|
-
|
|
29647
|
-
|
|
29648
|
-
|
|
29649
|
-
|
|
29650
|
-
|
|
29994
|
+
}
|
|
29995
|
+
function urlBasename(url2) {
|
|
29996
|
+
const last = parseUrl(url2)?.pathname.split("/").pop();
|
|
29997
|
+
if (!last)
|
|
29998
|
+
return "GIF";
|
|
29999
|
+
try {
|
|
30000
|
+
return decodeURIComponent(last);
|
|
30001
|
+
} catch {
|
|
30002
|
+
return last;
|
|
29651
30003
|
}
|
|
29652
|
-
|
|
29653
|
-
|
|
29654
|
-
|
|
29655
|
-
|
|
29656
|
-
|
|
29657
|
-
|
|
29658
|
-
|
|
29659
|
-
|
|
29660
|
-
|
|
29661
|
-
|
|
29662
|
-
|
|
29663
|
-
|
|
29664
|
-
|
|
29665
|
-
|
|
30004
|
+
}
|
|
30005
|
+
function gifToMetadata(gif) {
|
|
30006
|
+
return {
|
|
30007
|
+
url: gif.url,
|
|
30008
|
+
name: gif.title || urlBasename(gif.url),
|
|
30009
|
+
contentType: isKlipyVideoMediaUrl(gif.url) ? "video/mp4" : "image/gif",
|
|
30010
|
+
kind: "gif",
|
|
30011
|
+
...gif.width && gif.height ? { width: gif.width, height: gif.height } : {}
|
|
30012
|
+
};
|
|
30013
|
+
}
|
|
30014
|
+
|
|
30015
|
+
// src/commands/chat-gifs.ts
|
|
30016
|
+
var DEFAULT_LIMIT = 10;
|
|
30017
|
+
var MAX_TITLE = 40;
|
|
30018
|
+
function clampLimit(raw) {
|
|
30019
|
+
const n = Number.parseInt(raw, 10);
|
|
30020
|
+
if (!Number.isFinite(n))
|
|
30021
|
+
return DEFAULT_LIMIT;
|
|
30022
|
+
return Math.min(24, Math.max(1, n));
|
|
30023
|
+
}
|
|
30024
|
+
function truncate(text, max) {
|
|
30025
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
30026
|
+
}
|
|
30027
|
+
function registerChatGifsCommand(chat) {
|
|
30028
|
+
chat.command("gifs [query]").description("Search Klipy GIFs (trending when no query) — pass a result URL to `amiko chat send --gif`").option("--page <n>", "Result page (24 per page)", "1").option("--limit <n>", "Max results shown (1-24)", String(DEFAULT_LIMIT)).option("--raw", "Output raw JSON").action(async (query, opts) => {
|
|
30029
|
+
const auth = authOrExit();
|
|
30030
|
+
const q = query?.trim() ?? "";
|
|
30031
|
+
const page = Math.max(1, Number.parseInt(opts.page, 10) || 1);
|
|
30032
|
+
let data;
|
|
30033
|
+
try {
|
|
30034
|
+
data = await searchGifs(auth, q, page);
|
|
30035
|
+
} catch (e5) {
|
|
30036
|
+
console.error(error(e5 instanceof Error ? e5.message : "GIF search failed"));
|
|
30037
|
+
process.exit(1);
|
|
30038
|
+
}
|
|
30039
|
+
renderOutput(data, (d) => {
|
|
30040
|
+
const items = (d.items ?? []).slice(0, clampLimit(opts.limit));
|
|
30041
|
+
if (items.length === 0) {
|
|
30042
|
+
console.log(dim(q ? `No GIFs found for "${q}".` : "No trending GIFs."));
|
|
30043
|
+
console.log(dim("Powered by KLIPY"));
|
|
30044
|
+
return;
|
|
30045
|
+
}
|
|
30046
|
+
console.log(heading(q ? `GIFs matching "${q}" (page ${d.page})` : `Trending GIFs (page ${d.page})`));
|
|
30047
|
+
const rows = [
|
|
30048
|
+
[dim("TITLE"), dim("SIZE"), dim("URL")],
|
|
30049
|
+
...items.map((gif) => [
|
|
30050
|
+
truncate(gif.title || "GIF", MAX_TITLE),
|
|
30051
|
+
gif.width && gif.height ? `${gif.width}x${gif.height}` : "-",
|
|
30052
|
+
gif.gifUrl
|
|
30053
|
+
])
|
|
30054
|
+
];
|
|
30055
|
+
console.log(table(rows));
|
|
30056
|
+
console.log(dim(`
|
|
30057
|
+
Send one: amiko chat send <target> --gif <url>${d.hasNext ? ` (more: --page ${d.page + 1})` : ""}`));
|
|
30058
|
+
console.log(dim("Powered by KLIPY"));
|
|
30059
|
+
}, { json: opts.raw });
|
|
30060
|
+
});
|
|
30061
|
+
}
|
|
30062
|
+
|
|
30063
|
+
// src/commands/chat-lists.ts
|
|
30064
|
+
var collect2 = (v, prev) => [...prev, v];
|
|
30065
|
+
function exitWithError2(e5, fallback2) {
|
|
30066
|
+
console.error(error(e5 instanceof Error ? e5.message : fallback2));
|
|
30067
|
+
process.exit(1);
|
|
30068
|
+
}
|
|
30069
|
+
function conversationFlags(tokens) {
|
|
30070
|
+
return tokens.map((t) => ` --conversation ${JSON.stringify(t)}`).join("");
|
|
30071
|
+
}
|
|
30072
|
+
function countNoun(n) {
|
|
30073
|
+
return `${n} conversation${n === 1 ? "" : "s"}`;
|
|
30074
|
+
}
|
|
30075
|
+
async function fetchChatLists(auth) {
|
|
30076
|
+
const data = await amikoWebFetch(auth, "/api/chat-lists", { timeoutMs: 20000 });
|
|
30077
|
+
return data.chatLists ?? [];
|
|
30078
|
+
}
|
|
30079
|
+
async function resolveList(auth, target) {
|
|
30080
|
+
const t = target.trim();
|
|
30081
|
+
if (!t) {
|
|
30082
|
+
throw new Error("No list given. Run `amiko chat lists` and pass a list id or name.");
|
|
30083
|
+
}
|
|
30084
|
+
const all = await fetchChatLists(auth);
|
|
30085
|
+
if (looksLikeConversationId(t)) {
|
|
30086
|
+
const byId = all.find((l) => l.id === t);
|
|
30087
|
+
if (byId)
|
|
30088
|
+
return byId;
|
|
30089
|
+
}
|
|
30090
|
+
const lower = t.toLowerCase();
|
|
30091
|
+
const exact = all.filter((l) => l.name.toLowerCase() === lower);
|
|
30092
|
+
const matches = exact.length ? exact : all.filter((l) => l.name.toLowerCase().includes(lower));
|
|
30093
|
+
if (matches.length === 0) {
|
|
30094
|
+
throw new Error(`No chat list matching "${t}". Run \`amiko chat lists\` to see them.`);
|
|
30095
|
+
}
|
|
30096
|
+
if (matches.length > 1) {
|
|
30097
|
+
const names = matches.map((l) => `"${l.name}" (${l.id})`).join(", ");
|
|
30098
|
+
throw new Error(`"${t}" is ambiguous — matches: ${names}. Use the list's id.`);
|
|
30099
|
+
}
|
|
30100
|
+
return matches[0];
|
|
30101
|
+
}
|
|
30102
|
+
async function resolveListOrExit(auth, target) {
|
|
30103
|
+
try {
|
|
30104
|
+
return await resolveList(auth, target);
|
|
30105
|
+
} catch (e5) {
|
|
30106
|
+
exitWithError2(e5, "Could not resolve the chat list");
|
|
30107
|
+
}
|
|
30108
|
+
}
|
|
30109
|
+
async function resolveEntriesOrExit(auth, tokens) {
|
|
30110
|
+
if (tokens.length === 0) {
|
|
30111
|
+
console.error(error("At least one --conversation is required."));
|
|
30112
|
+
process.exit(1);
|
|
30113
|
+
}
|
|
30114
|
+
const entries = [];
|
|
30115
|
+
const failures = [];
|
|
30116
|
+
const seen = new Set;
|
|
30117
|
+
for (const raw of tokens) {
|
|
30118
|
+
try {
|
|
30119
|
+
const resolved = await resolveConversation(auth, raw);
|
|
30120
|
+
let entry;
|
|
30121
|
+
if (resolved.kind === "conversation") {
|
|
30122
|
+
entry = { id: resolved.id, label: resolved.label };
|
|
30123
|
+
} else {
|
|
30124
|
+
const existing = await findDmByUserId(auth, resolved.userId);
|
|
30125
|
+
if (!existing) {
|
|
30126
|
+
throw new Error(`no ${resolved.label} exists yet — a chat list can only hold existing conversations.`);
|
|
29666
30127
|
}
|
|
29667
|
-
|
|
29668
|
-
throw e5;
|
|
30128
|
+
entry = { id: existing, label: resolved.label };
|
|
29669
30129
|
}
|
|
30130
|
+
if (seen.has(entry.id))
|
|
30131
|
+
continue;
|
|
30132
|
+
seen.add(entry.id);
|
|
30133
|
+
entries.push(entry);
|
|
30134
|
+
} catch (e5) {
|
|
30135
|
+
failures.push({
|
|
30136
|
+
token: raw,
|
|
30137
|
+
reason: e5 instanceof Error ? e5.message : "could not be resolved"
|
|
30138
|
+
});
|
|
29670
30139
|
}
|
|
29671
|
-
|
|
29672
|
-
|
|
29673
|
-
|
|
30140
|
+
}
|
|
30141
|
+
if (failures.length > 0) {
|
|
30142
|
+
console.error(error("Could not resolve every conversation — nothing was changed."));
|
|
30143
|
+
for (const entry of entries) {
|
|
30144
|
+
console.error(dim(` resolved: ${entry.label} → ${entry.id}`));
|
|
29674
30145
|
}
|
|
29675
|
-
|
|
30146
|
+
for (const f of failures) {
|
|
30147
|
+
console.error(error(` ${JSON.stringify(f.token)}: ${f.reason}`));
|
|
30148
|
+
}
|
|
30149
|
+
process.exit(1);
|
|
29676
30150
|
}
|
|
29677
|
-
|
|
29678
|
-
|
|
29679
|
-
|
|
29680
|
-
const
|
|
29681
|
-
|
|
29682
|
-
const
|
|
29683
|
-
|
|
30151
|
+
return entries;
|
|
30152
|
+
}
|
|
30153
|
+
async function conversationLabels(auth) {
|
|
30154
|
+
const labels = new Map;
|
|
30155
|
+
try {
|
|
30156
|
+
const data = await amikoWebFetch(auth, "/api/conversations", { query: { limit: 100, archived: "all" }, timeoutMs: 20000 });
|
|
30157
|
+
for (const c of data.conversations ?? []) {
|
|
30158
|
+
labels.set(c.id, convLabel(c, auth.userId));
|
|
30159
|
+
}
|
|
30160
|
+
} catch {}
|
|
30161
|
+
return labels;
|
|
30162
|
+
}
|
|
30163
|
+
function listNotFound(e5) {
|
|
30164
|
+
return e5.status === 404;
|
|
30165
|
+
}
|
|
30166
|
+
function registerChatListsCommand(lists) {
|
|
30167
|
+
lists.option("--raw", "Output raw JSON").action(async (opts) => {
|
|
30168
|
+
const auth = authOrExit();
|
|
30169
|
+
let all;
|
|
30170
|
+
try {
|
|
30171
|
+
all = await fetchChatLists(auth);
|
|
30172
|
+
} catch (e5) {
|
|
30173
|
+
exitWithError2(e5, "Failed to load chat lists");
|
|
30174
|
+
}
|
|
30175
|
+
if (opts.raw) {
|
|
30176
|
+
console.log(JSON.stringify({ chatLists: all }, null, 2));
|
|
30177
|
+
return;
|
|
30178
|
+
}
|
|
30179
|
+
if (all.length === 0) {
|
|
30180
|
+
console.log(dim("No chat lists."));
|
|
30181
|
+
console.log(dim("Create one with `amiko chat lists create <name> --conversation <who>`."));
|
|
30182
|
+
return;
|
|
30183
|
+
}
|
|
30184
|
+
const labels = await conversationLabels(auth);
|
|
30185
|
+
console.log(heading(`Chat lists (${all.length})
|
|
30186
|
+
`));
|
|
30187
|
+
for (const l of all) {
|
|
30188
|
+
const ids = l.conversation_ids ?? [];
|
|
30189
|
+
console.log(label(l.name, dim(`${l.id} · ${countNoun(ids.length)}`)));
|
|
30190
|
+
for (const id of ids) {
|
|
30191
|
+
const name = labels.get(id);
|
|
30192
|
+
console.log(dim(name ? ` - ${name} (${id})` : ` - ${id}`));
|
|
30193
|
+
}
|
|
30194
|
+
}
|
|
30195
|
+
});
|
|
30196
|
+
lists.command("create <name>").description("Create a chat list, optionally with initial conversations").option("--conversation <target>", "Conversation to include (conversation id, user id, @handle, group title, or name); repeat per conversation", collect2, []).option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (name, opts) => {
|
|
30197
|
+
const auth = authOrExit();
|
|
30198
|
+
const entries = opts.conversation.length > 0 ? await resolveEntriesOrExit(auth, opts.conversation) : [];
|
|
30199
|
+
await confirmDestructive({
|
|
30200
|
+
action: `Create chat list "${name}"${entries.length ? ` with ${entries.map((e5) => e5.label).join(", ")}` : " (empty)"}`,
|
|
30201
|
+
detail: "Chat lists are private folders — only you see them, in every Amiko app.",
|
|
30202
|
+
yes: opts.yes,
|
|
30203
|
+
commandExample: `amiko chat lists create ${JSON.stringify(name)}${conversationFlags(opts.conversation)}`
|
|
30204
|
+
});
|
|
30205
|
+
let data;
|
|
30206
|
+
try {
|
|
30207
|
+
data = await amikoWebFetch(auth, "/api/chat-lists", {
|
|
30208
|
+
method: "POST",
|
|
30209
|
+
body: { name, conversation_ids: entries.map((e5) => e5.id) },
|
|
30210
|
+
timeoutMs: 20000
|
|
30211
|
+
});
|
|
30212
|
+
} catch (e5) {
|
|
30213
|
+
exitWithError2(e5, "Failed to create the chat list");
|
|
30214
|
+
}
|
|
30215
|
+
if (opts.raw) {
|
|
30216
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30217
|
+
return;
|
|
30218
|
+
}
|
|
30219
|
+
console.log(success(`Chat list created: ${name}`));
|
|
30220
|
+
if (data.chatList?.id)
|
|
30221
|
+
console.log(label("ID", data.chatList.id));
|
|
30222
|
+
if (entries.length) {
|
|
30223
|
+
console.log(label("Conversations", entries.map((e5) => e5.label).join(", ")));
|
|
30224
|
+
}
|
|
30225
|
+
});
|
|
30226
|
+
lists.command("rename <list> <newName>").description("Rename a chat list (list id or name)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, newName, opts) => {
|
|
30227
|
+
const auth = authOrExit();
|
|
30228
|
+
const list = await resolveListOrExit(auth, target);
|
|
30229
|
+
await confirmDestructive({
|
|
30230
|
+
action: `Rename chat list "${list.name}" to "${newName}"`,
|
|
30231
|
+
yes: opts.yes,
|
|
30232
|
+
commandExample: `amiko chat lists rename ${JSON.stringify(target)} ${JSON.stringify(newName)}`
|
|
30233
|
+
});
|
|
30234
|
+
let data;
|
|
30235
|
+
try {
|
|
30236
|
+
data = await amikoWebFetch(auth, `/api/chat-lists/${encodeURIComponent(list.id)}`, { method: "PATCH", body: { name: newName }, timeoutMs: 20000 });
|
|
30237
|
+
} catch (e5) {
|
|
30238
|
+
exitWithError2(e5, "Rename failed");
|
|
30239
|
+
}
|
|
30240
|
+
if (opts.raw) {
|
|
30241
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30242
|
+
return;
|
|
30243
|
+
}
|
|
30244
|
+
console.log(success(`Chat list renamed to: ${newName}`) + dim(` (${list.id})`));
|
|
30245
|
+
});
|
|
30246
|
+
lists.command("add <list>").description("Add conversations to a chat list (list id or name)").option("--conversation <target>", "Conversation to add (conversation id, user id, @handle, group title, or name); repeat per conversation", collect2, []).option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
30247
|
+
const auth = authOrExit();
|
|
30248
|
+
const entries = await resolveEntriesOrExit(auth, opts.conversation);
|
|
30249
|
+
const list = await resolveListOrExit(auth, target);
|
|
30250
|
+
const current = list.conversation_ids ?? [];
|
|
30251
|
+
const currentSet = new Set(current);
|
|
30252
|
+
const fresh = entries.filter((e5) => !currentSet.has(e5.id));
|
|
30253
|
+
const already = entries.filter((e5) => currentSet.has(e5.id));
|
|
30254
|
+
if (fresh.length === 0) {
|
|
30255
|
+
console.log(dim(`Already in "${list.name}" — nothing to add.`));
|
|
30256
|
+
return;
|
|
30257
|
+
}
|
|
30258
|
+
await confirmDestructive({
|
|
30259
|
+
action: `Add ${fresh.map((e5) => e5.label).join(", ")} to chat list "${list.name}"`,
|
|
30260
|
+
yes: opts.yes,
|
|
30261
|
+
commandExample: `amiko chat lists add ${JSON.stringify(target)}${conversationFlags(opts.conversation)}`
|
|
30262
|
+
});
|
|
30263
|
+
let data;
|
|
30264
|
+
try {
|
|
30265
|
+
data = await amikoWebFetch(auth, `/api/chat-lists/${encodeURIComponent(list.id)}`, {
|
|
30266
|
+
method: "PATCH",
|
|
30267
|
+
body: {
|
|
30268
|
+
conversation_ids: [...current, ...fresh.map((e5) => e5.id)]
|
|
30269
|
+
},
|
|
30270
|
+
timeoutMs: 20000
|
|
30271
|
+
});
|
|
30272
|
+
} catch (e5) {
|
|
30273
|
+
if (listNotFound(e5)) {
|
|
30274
|
+
exitWithError2(null, `Chat list "${list.name}" no longer exists.`);
|
|
30275
|
+
}
|
|
30276
|
+
exitWithError2(e5, "Failed to update the chat list");
|
|
30277
|
+
}
|
|
30278
|
+
if (opts.raw) {
|
|
30279
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30280
|
+
return;
|
|
30281
|
+
}
|
|
30282
|
+
console.log(success(`Added ${fresh.map((e5) => e5.label).join(", ")} to "${list.name}"`) + dim(` (now ${countNoun(current.length + fresh.length)})`));
|
|
30283
|
+
for (const e5 of already) {
|
|
30284
|
+
console.log(dim(` ${e5.label} was already in the list.`));
|
|
30285
|
+
}
|
|
30286
|
+
});
|
|
30287
|
+
lists.command("remove <list>").description("Remove conversations from a chat list (the conversations themselves are untouched)").option("--conversation <target>", "Conversation to remove (conversation id, user id, @handle, group title, or name); repeat per conversation", collect2, []).option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
30288
|
+
const auth = authOrExit();
|
|
30289
|
+
const entries = await resolveEntriesOrExit(auth, opts.conversation);
|
|
30290
|
+
const list = await resolveListOrExit(auth, target);
|
|
30291
|
+
const current = list.conversation_ids ?? [];
|
|
30292
|
+
const currentSet = new Set(current);
|
|
30293
|
+
const inList = entries.filter((e5) => currentSet.has(e5.id));
|
|
30294
|
+
const notInList = entries.filter((e5) => !currentSet.has(e5.id));
|
|
30295
|
+
if (inList.length === 0) {
|
|
30296
|
+
console.log(dim(`None of those are in "${list.name}".`));
|
|
30297
|
+
return;
|
|
30298
|
+
}
|
|
30299
|
+
await confirmDestructive({
|
|
30300
|
+
action: `Remove ${inList.map((e5) => e5.label).join(", ")} from chat list "${list.name}"`,
|
|
30301
|
+
yes: opts.yes,
|
|
30302
|
+
commandExample: `amiko chat lists remove ${JSON.stringify(target)}${conversationFlags(opts.conversation)}`
|
|
30303
|
+
});
|
|
30304
|
+
const removeIds = new Set(inList.map((e5) => e5.id));
|
|
30305
|
+
let data;
|
|
30306
|
+
try {
|
|
30307
|
+
data = await amikoWebFetch(auth, `/api/chat-lists/${encodeURIComponent(list.id)}`, {
|
|
30308
|
+
method: "PATCH",
|
|
30309
|
+
body: {
|
|
30310
|
+
conversation_ids: current.filter((id) => !removeIds.has(id))
|
|
30311
|
+
},
|
|
30312
|
+
timeoutMs: 20000
|
|
30313
|
+
});
|
|
30314
|
+
} catch (e5) {
|
|
30315
|
+
if (listNotFound(e5)) {
|
|
30316
|
+
exitWithError2(null, `Chat list "${list.name}" no longer exists.`);
|
|
30317
|
+
}
|
|
30318
|
+
exitWithError2(e5, "Failed to update the chat list");
|
|
30319
|
+
}
|
|
30320
|
+
if (opts.raw) {
|
|
30321
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30322
|
+
return;
|
|
30323
|
+
}
|
|
30324
|
+
console.log(success(`Removed ${inList.map((e5) => e5.label).join(", ")} from "${list.name}"`) + dim(` (now ${countNoun(current.length - inList.length)})`));
|
|
30325
|
+
for (const e5 of notInList) {
|
|
30326
|
+
console.log(dim(` ${e5.label} was not in the list.`));
|
|
30327
|
+
}
|
|
30328
|
+
});
|
|
30329
|
+
lists.command("delete <list>").description("Delete a chat list (the conversations in it are untouched)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
30330
|
+
const auth = authOrExit();
|
|
30331
|
+
const list = await resolveListOrExit(auth, target);
|
|
30332
|
+
const count = (list.conversation_ids ?? []).length;
|
|
30333
|
+
await confirmDestructive({
|
|
30334
|
+
action: `Delete chat list "${list.name}" (${countNoun(count)})`,
|
|
30335
|
+
detail: "The list's name and membership are gone for good; the conversations themselves are not affected.",
|
|
30336
|
+
yes: opts.yes,
|
|
30337
|
+
commandExample: `amiko chat lists delete ${JSON.stringify(target)}`
|
|
30338
|
+
});
|
|
30339
|
+
let data;
|
|
30340
|
+
try {
|
|
30341
|
+
data = await amikoWebFetch(auth, `/api/chat-lists/${encodeURIComponent(list.id)}`, { method: "DELETE", timeoutMs: 20000 });
|
|
30342
|
+
} catch (e5) {
|
|
30343
|
+
exitWithError2(e5, "Failed to delete the chat list");
|
|
30344
|
+
}
|
|
30345
|
+
if (opts.raw) {
|
|
30346
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30347
|
+
return;
|
|
30348
|
+
}
|
|
30349
|
+
console.log(success(`Chat list deleted: ${list.name}`));
|
|
30350
|
+
});
|
|
30351
|
+
}
|
|
30352
|
+
|
|
30353
|
+
// src/commands/chat.ts
|
|
30354
|
+
var PIN_LIMIT = 20;
|
|
30355
|
+
function pinErrorMessage(e5, fallback2) {
|
|
30356
|
+
const status = e5.status;
|
|
30357
|
+
if (status === 403) {
|
|
30358
|
+
return "Only group admins can pin or unpin messages in a group — check roles with `amiko chat group info`. In a DM either side can pin.";
|
|
29684
30359
|
}
|
|
29685
|
-
|
|
29686
|
-
|
|
30360
|
+
if (status === 409) {
|
|
30361
|
+
return `Pin limit reached (${PIN_LIMIT}) — unpin one first (see them with \`amiko chat pinned\`).`;
|
|
30362
|
+
}
|
|
30363
|
+
if (status === 400) {
|
|
30364
|
+
return "This message can't be pinned (system and in-flight messages aren't pinnable).";
|
|
30365
|
+
}
|
|
30366
|
+
return e5 instanceof Error ? e5.message : fallback2;
|
|
30367
|
+
}
|
|
30368
|
+
async function findOrCreateDm(auth, userId) {
|
|
30369
|
+
const data = await amikoWebFetch(auth, "/api/conversations", {
|
|
30370
|
+
method: "POST",
|
|
30371
|
+
body: {
|
|
30372
|
+
participant_ids: [userId],
|
|
30373
|
+
participant_types: ["user"],
|
|
30374
|
+
conversation_type: "direct"
|
|
30375
|
+
},
|
|
30376
|
+
timeoutMs: 20000
|
|
30377
|
+
});
|
|
30378
|
+
const id = data.conversation?.id;
|
|
30379
|
+
if (!id)
|
|
30380
|
+
throw new Error("Could not open a conversation with that user.");
|
|
30381
|
+
return id;
|
|
29687
30382
|
}
|
|
29688
30383
|
var HTTPS_RE = /^https:\/\//i;
|
|
30384
|
+
var URL_SHAPED_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
29689
30385
|
var CHAT_IMAGE_MIME = new Set([
|
|
29690
30386
|
"image/jpeg",
|
|
29691
30387
|
"image/jpg",
|
|
@@ -29732,45 +30428,466 @@ async function uploadChatVoice(auth, convId, path2) {
|
|
|
29732
30428
|
await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(convId)}/audio/upload`, { method: "POST", multipart: form, timeoutMs: 120000 });
|
|
29733
30429
|
}
|
|
29734
30430
|
function registerChatCommand(chat) {
|
|
29735
|
-
chat.command("list").description("List your conversations (DMs and group chats)").option("--limit <n>", "Max conversations", "30").option("--archived", "Include archived conversations").option("--raw", "Output raw JSON").action(async (opts) => {
|
|
30431
|
+
chat.command("list").description("List your conversations (DMs and group chats)").option("--limit <n>", "Max conversations", "30").option("--archived", "Include archived conversations").option("--mentions", "Only conversations with an unread @mention of you, a permitted @all, or a reply to one of your messages").option("--raw", "Output raw JSON").action(async (opts) => {
|
|
30432
|
+
const auth = authOrExit();
|
|
30433
|
+
let data;
|
|
30434
|
+
try {
|
|
30435
|
+
data = await amikoWebFetch(auth, "/api/conversations", {
|
|
30436
|
+
query: {
|
|
30437
|
+
limit: opts.limit,
|
|
30438
|
+
...opts.archived ? { archived: "all" } : {}
|
|
30439
|
+
},
|
|
30440
|
+
timeoutMs: 20000
|
|
30441
|
+
});
|
|
30442
|
+
} catch (e5) {
|
|
30443
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to list conversations"));
|
|
30444
|
+
process.exit(1);
|
|
30445
|
+
}
|
|
30446
|
+
const mentionsMe = (c) => !!c.unread_count && !!c.has_unread_mention;
|
|
30447
|
+
const convs = opts.mentions ? (data.conversations ?? []).filter(mentionsMe) : data.conversations ?? [];
|
|
30448
|
+
if (opts.raw) {
|
|
30449
|
+
console.log(JSON.stringify(opts.mentions ? { ...data, conversations: convs } : data, null, 2));
|
|
30450
|
+
return;
|
|
30451
|
+
}
|
|
30452
|
+
if (convs.length === 0) {
|
|
30453
|
+
console.log(dim(opts.mentions ? "No conversations with unread mentions or replies to you." : "No conversations."));
|
|
30454
|
+
return;
|
|
30455
|
+
}
|
|
30456
|
+
console.log(heading(`Conversations (${convs.length})
|
|
30457
|
+
`));
|
|
30458
|
+
for (const c of convs) {
|
|
30459
|
+
const kind = c.conversation_type === "direct" ? "DM" : c.conversation_type;
|
|
30460
|
+
const unread = c.unread_count ? ` (${c.unread_count} unread${mentionsMe(c) ? ", @you" : ""})` : "";
|
|
30461
|
+
console.log(label(`[${kind}] ${convLabel(c, auth.userId)}`, dim(c.id)));
|
|
30462
|
+
if (c.last_message?.content) {
|
|
30463
|
+
const who = c.last_message.name ? `${c.last_message.name}: ` : "";
|
|
30464
|
+
const preview = mentionMarkupToPlainText(c.last_message.content);
|
|
30465
|
+
console.log(dim(` ${who}${preview.slice(0, 80)}`));
|
|
30466
|
+
}
|
|
30467
|
+
if (unread)
|
|
30468
|
+
console.log(dim(` ${unread.trim()}`));
|
|
30469
|
+
}
|
|
30470
|
+
});
|
|
30471
|
+
chat.command("read <target>").description("Read recent messages in a conversation (conversation id, user id, @handle, or name)").option("--limit <n>", "Max messages", "20").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
30472
|
+
const auth = authOrExit();
|
|
30473
|
+
let convId;
|
|
30474
|
+
try {
|
|
30475
|
+
const resolved = await resolveConversation(auth, target);
|
|
30476
|
+
if (resolved.kind === "user") {
|
|
30477
|
+
const existing = await findDmByUserId(auth, resolved.userId);
|
|
30478
|
+
if (!existing) {
|
|
30479
|
+
throw new Error(`No ${resolved.label} yet — start one with \`amiko chat send\`.`);
|
|
30480
|
+
}
|
|
30481
|
+
convId = existing;
|
|
30482
|
+
} else {
|
|
30483
|
+
convId = resolved.id;
|
|
30484
|
+
}
|
|
30485
|
+
} catch (e5) {
|
|
30486
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
30487
|
+
process.exit(1);
|
|
30488
|
+
}
|
|
30489
|
+
let data;
|
|
30490
|
+
try {
|
|
30491
|
+
data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(convId)}/messages`, { query: { limit: opts.limit }, timeoutMs: 20000 });
|
|
30492
|
+
} catch (e5) {
|
|
30493
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to read messages"));
|
|
30494
|
+
process.exit(1);
|
|
30495
|
+
}
|
|
30496
|
+
if (opts.raw) {
|
|
30497
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30498
|
+
return;
|
|
30499
|
+
}
|
|
30500
|
+
const msgs = (data.messages ?? []).slice().reverse();
|
|
30501
|
+
if (msgs.length === 0) {
|
|
30502
|
+
console.log(dim("No messages."));
|
|
30503
|
+
return;
|
|
30504
|
+
}
|
|
30505
|
+
console.log(heading(`Messages (${msgs.length})
|
|
30506
|
+
`));
|
|
30507
|
+
for (const m of msgs) {
|
|
30508
|
+
const who = m.sender?.name ?? m.name ?? (m.sender_id ? m.sender_id.slice(0, 8) : "?");
|
|
30509
|
+
const when = m.created_at ? m.created_at.slice(0, 19).replace("T", " ") : "";
|
|
30510
|
+
console.log(label(who, mentionMarkupToPlainText(m.content ?? "")));
|
|
30511
|
+
const meta3 = [when, `id ${m.id}`].filter(Boolean).join(" ");
|
|
30512
|
+
console.log(dim(` ${meta3}`));
|
|
30513
|
+
if (m.reactions?.length) {
|
|
30514
|
+
console.log(dim(` reactions: ${summarizeReactions(m.reactions)}`));
|
|
30515
|
+
}
|
|
30516
|
+
}
|
|
30517
|
+
});
|
|
30518
|
+
chat.command("send <target> [message]").description("Send a message as yourself (the owner) to a conversation or person (conversation id, user id, @handle, or name — DMs are opened automatically). The message is optional only with --gif.").option("--image <pathOrUrl>", "Attach an image: local file (jpg/png/webp/gif, ≤10MB) or an Amiko URL (e.g. from `amiko create`)").option("--audio <pathOrUrl>", "Attach audio as a voice note: local file (≤25MB) or an Amiko URL").option("--gif <queryOrUrl>", "Attach a GIF: search words (sends the top Klipy result) or a Klipy URL from `amiko chat gifs`").option("--all", "Mention @all — notifies every member of the group (admins/owners always; others only when enabled via `amiko chat group mention-all`)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, message, opts) => {
|
|
29736
30519
|
const auth = authOrExit();
|
|
30520
|
+
const mediaFlagCount = [opts.image, opts.audio, opts.gif].filter(Boolean).length;
|
|
30521
|
+
if (mediaFlagCount > 1) {
|
|
30522
|
+
console.error(error("Attach only one of --image, --audio, or --gif (one media block per message)."));
|
|
30523
|
+
process.exit(1);
|
|
30524
|
+
}
|
|
30525
|
+
if (message === undefined && !opts.gif) {
|
|
30526
|
+
console.error(error("A message is required (it may only be omitted when sending a GIF with --gif)."));
|
|
30527
|
+
process.exit(1);
|
|
30528
|
+
}
|
|
30529
|
+
let resolved;
|
|
30530
|
+
try {
|
|
30531
|
+
resolved = await resolveConversation(auth, target);
|
|
30532
|
+
} catch (e5) {
|
|
30533
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
30534
|
+
process.exit(1);
|
|
30535
|
+
}
|
|
30536
|
+
const wantsAll = Boolean(opts.all);
|
|
30537
|
+
const messageText = message ?? "";
|
|
30538
|
+
let outgoing = messageText;
|
|
30539
|
+
let mentionAllActive = false;
|
|
30540
|
+
if (wantsAll || hasMentionAllLiteral(messageText) || hasMentionAllToken(messageText)) {
|
|
30541
|
+
const plainTextNote = "Note: @all only notifies people in group chats — sending it as plain text.";
|
|
30542
|
+
let conv = null;
|
|
30543
|
+
if (resolved.kind === "conversation") {
|
|
30544
|
+
try {
|
|
30545
|
+
conv = resolved.conversation ?? await fetchConversationById(auth, resolved.id);
|
|
30546
|
+
} catch (e5) {
|
|
30547
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load the conversation"));
|
|
30548
|
+
process.exit(1);
|
|
30549
|
+
}
|
|
30550
|
+
}
|
|
30551
|
+
if (!conv || conv.conversation_type !== "group") {
|
|
30552
|
+
if (wantsAll) {
|
|
30553
|
+
console.error(error("@all only works in group chats — this target is a direct message."));
|
|
30554
|
+
process.exit(1);
|
|
30555
|
+
}
|
|
30556
|
+
console.log(dim(plainTextNote));
|
|
30557
|
+
} else if (!canMentionAll(conv, auth.userId)) {
|
|
30558
|
+
if (wantsAll) {
|
|
30559
|
+
console.error(error(`You can't @all in "${conv.title ?? conv.id}" — only group admins can, unless an admin allows everyone with \`amiko chat group mention-all ${JSON.stringify(target)} on\`.`));
|
|
30560
|
+
process.exit(1);
|
|
30561
|
+
}
|
|
30562
|
+
console.log(dim('Note: you don\'t have @all permission in this group — "@all" goes out as plain text. An admin can enable it with `amiko chat group mention-all <group> on`.'));
|
|
30563
|
+
} else {
|
|
30564
|
+
outgoing = rewriteMentionAllLiteral(messageText);
|
|
30565
|
+
if (wantsAll)
|
|
30566
|
+
outgoing = applyMentionAll(outgoing);
|
|
30567
|
+
mentionAllActive = true;
|
|
30568
|
+
}
|
|
30569
|
+
}
|
|
30570
|
+
let gifAttachment = null;
|
|
30571
|
+
if (opts.gif) {
|
|
30572
|
+
try {
|
|
30573
|
+
if (URL_SHAPED_RE.test(opts.gif)) {
|
|
30574
|
+
if (!HTTPS_RE.test(opts.gif) || !isKlipyMediaUrl(opts.gif)) {
|
|
30575
|
+
throw new Error("--gif accepts search words or an https Klipy URL (from `amiko chat gifs`). For other images use --image with a local file or an Amiko URL.");
|
|
30576
|
+
}
|
|
30577
|
+
gifAttachment = gifToMetadata({ url: opts.gif });
|
|
30578
|
+
} else {
|
|
30579
|
+
const results = await searchGifs(auth, opts.gif);
|
|
30580
|
+
const top = results.items?.[0];
|
|
30581
|
+
if (!top) {
|
|
30582
|
+
throw new Error(`No GIFs found for "${opts.gif}" — try different words or browse with \`amiko chat gifs\`.`);
|
|
30583
|
+
}
|
|
30584
|
+
gifAttachment = gifToMetadata({
|
|
30585
|
+
url: top.gifUrl,
|
|
30586
|
+
title: top.title,
|
|
30587
|
+
width: top.width,
|
|
30588
|
+
height: top.height
|
|
30589
|
+
});
|
|
30590
|
+
}
|
|
30591
|
+
} catch (e5) {
|
|
30592
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve the GIF"));
|
|
30593
|
+
process.exit(1);
|
|
30594
|
+
}
|
|
30595
|
+
}
|
|
30596
|
+
const media = opts.image ? `image ${opts.image}` : opts.audio ? `audio ${opts.audio}` : gifAttachment ? `gif "${gifAttachment.name.slice(0, 40)}"` : "";
|
|
30597
|
+
const shown = mentionMarkupToPlainText(outgoing);
|
|
30598
|
+
await requireApproval({
|
|
30599
|
+
free: "sends a message as you (the owner) — the recipients see it",
|
|
30600
|
+
summary: `Send to ${resolved.label}${media ? ` [+${media}]` : ""}${mentionAllActive ? " [@all — notifies every member]" : ""}: ${shown.slice(0, 80)}${shown.length > 80 ? "…" : ""}`,
|
|
30601
|
+
yes: opts.yes,
|
|
30602
|
+
commandExample: `amiko chat send ${JSON.stringify(target)}${message !== undefined ? ` ${JSON.stringify(message)}` : ""}${opts.image ? ` --image ${JSON.stringify(opts.image)}` : ""}${opts.audio ? ` --audio ${JSON.stringify(opts.audio)}` : ""}${opts.gif ? ` --gif ${JSON.stringify(opts.gif)}` : ""}${opts.all ? " --all" : ""}`
|
|
30603
|
+
});
|
|
30604
|
+
let convId;
|
|
30605
|
+
try {
|
|
30606
|
+
convId = resolved.kind === "user" ? await findOrCreateDm(auth, resolved.userId) : resolved.id;
|
|
30607
|
+
} catch (e5) {
|
|
30608
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not open the conversation"));
|
|
30609
|
+
process.exit(1);
|
|
30610
|
+
}
|
|
30611
|
+
const messagesPath = `/api/conversations/${encodeURIComponent(convId)}/messages`;
|
|
30612
|
+
const post = (body) => amikoWebFetch(auth, messagesPath, {
|
|
30613
|
+
method: "POST",
|
|
30614
|
+
body,
|
|
30615
|
+
timeoutMs: 20000
|
|
30616
|
+
});
|
|
30617
|
+
let data;
|
|
30618
|
+
try {
|
|
30619
|
+
if (gifAttachment) {
|
|
30620
|
+
const isVideo = isKlipyVideoMediaUrl(gifAttachment.url);
|
|
30621
|
+
data = await post({
|
|
30622
|
+
content: outgoing || "GIF",
|
|
30623
|
+
message_type: isVideo ? "video" : "image",
|
|
30624
|
+
metadata: {
|
|
30625
|
+
[isVideo ? "videos" : "images"]: [gifAttachment],
|
|
30626
|
+
image_caption: outgoing || null
|
|
30627
|
+
}
|
|
30628
|
+
});
|
|
30629
|
+
} else if (opts.image) {
|
|
30630
|
+
const img = HTTPS_RE.test(opts.image) ? { url: opts.image, name: urlName(opts.image) } : await uploadChatImage(auth, convId, opts.image);
|
|
30631
|
+
data = await post({
|
|
30632
|
+
content: outgoing,
|
|
30633
|
+
metadata: { images: [img], image_caption: outgoing || null }
|
|
30634
|
+
});
|
|
30635
|
+
} else if (opts.audio) {
|
|
30636
|
+
if (HTTPS_RE.test(opts.audio)) {
|
|
30637
|
+
data = await post({
|
|
30638
|
+
content: outgoing,
|
|
30639
|
+
message_type: "voice",
|
|
30640
|
+
metadata: { audioUrl: opts.audio }
|
|
30641
|
+
});
|
|
30642
|
+
} else {
|
|
30643
|
+
data = await post({ content: outgoing });
|
|
30644
|
+
await uploadChatVoice(auth, convId, opts.audio);
|
|
30645
|
+
}
|
|
30646
|
+
} else {
|
|
30647
|
+
data = await post({ content: outgoing });
|
|
30648
|
+
}
|
|
30649
|
+
} catch (e5) {
|
|
30650
|
+
console.error(error(e5 instanceof Error ? e5.message : "Send failed"));
|
|
30651
|
+
process.exit(1);
|
|
30652
|
+
}
|
|
30653
|
+
if (opts.raw) {
|
|
30654
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30655
|
+
return;
|
|
30656
|
+
}
|
|
30657
|
+
const what = opts.image ? "Message + image sent" : opts.audio ? "Message + voice note sent" : gifAttachment ? outgoing ? "Message + GIF sent" : "GIF sent" : "Message sent";
|
|
30658
|
+
console.log(success(what) + dim(` (conversation ${convId.slice(0, 8)}…)`));
|
|
30659
|
+
});
|
|
30660
|
+
chat.command("react <messageId> <emoji>").description("React to a message as yourself (the owner). Get the message id from `amiko chat read`. One reaction per message — a new emoji replaces your previous one.").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (messageId, emojiArg, opts) => {
|
|
30661
|
+
const auth = authOrExit();
|
|
30662
|
+
let emoji3;
|
|
30663
|
+
try {
|
|
30664
|
+
emoji3 = validateEmoji(emojiArg);
|
|
30665
|
+
} catch (e5) {
|
|
30666
|
+
console.error(error(e5 instanceof Error ? e5.message : "Invalid emoji"));
|
|
30667
|
+
process.exit(1);
|
|
30668
|
+
}
|
|
30669
|
+
let msg;
|
|
30670
|
+
try {
|
|
30671
|
+
msg = await fetchMessage(auth, messageId);
|
|
30672
|
+
} catch (e5) {
|
|
30673
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load that message"));
|
|
30674
|
+
process.exit(1);
|
|
30675
|
+
}
|
|
30676
|
+
if (ownReaction(msg.reactions, auth.userId) === emoji3) {
|
|
30677
|
+
if (opts.raw) {
|
|
30678
|
+
console.log(JSON.stringify({ success: true, action: "unchanged", reactions: msg.reactions }, null, 2));
|
|
30679
|
+
return;
|
|
30680
|
+
}
|
|
30681
|
+
console.log(success(`Already reacted with ${emoji3}`));
|
|
30682
|
+
return;
|
|
30683
|
+
}
|
|
30684
|
+
await requireApproval({
|
|
30685
|
+
free: "adds your reaction — people in the chat see it",
|
|
30686
|
+
summary: `React ${emoji3} to "${previewText(msg)}"`,
|
|
30687
|
+
yes: opts.yes,
|
|
30688
|
+
commandExample: `amiko chat react ${JSON.stringify(messageId)} ${JSON.stringify(emoji3)}`
|
|
30689
|
+
});
|
|
30690
|
+
let data;
|
|
30691
|
+
try {
|
|
30692
|
+
data = await amikoWebFetch(auth, `/api/messages/${encodeURIComponent(messageId)}/reactions`, { method: "POST", body: { emoji: emoji3 }, timeoutMs: 20000 });
|
|
30693
|
+
} catch (e5) {
|
|
30694
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to react"));
|
|
30695
|
+
process.exit(1);
|
|
30696
|
+
}
|
|
30697
|
+
if (opts.raw) {
|
|
30698
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30699
|
+
return;
|
|
30700
|
+
}
|
|
30701
|
+
const what = data.action === "removed" ? `Removed your ${emoji3} reaction` : data.action === "updated" ? `Reaction changed to ${emoji3}` : `Reacted ${emoji3}`;
|
|
30702
|
+
console.log(success(what));
|
|
30703
|
+
});
|
|
30704
|
+
chat.command("unreact <messageId>").description("Remove your (the owner's) reaction from a message. Get the message id from `amiko chat read`.").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (messageId, opts) => {
|
|
30705
|
+
const auth = authOrExit();
|
|
30706
|
+
let msg;
|
|
30707
|
+
try {
|
|
30708
|
+
msg = await fetchMessage(auth, messageId);
|
|
30709
|
+
} catch (e5) {
|
|
30710
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load that message"));
|
|
30711
|
+
process.exit(1);
|
|
30712
|
+
}
|
|
30713
|
+
const mine = ownReaction(msg.reactions, auth.userId);
|
|
30714
|
+
if (!mine) {
|
|
30715
|
+
if (opts.raw) {
|
|
30716
|
+
console.log(JSON.stringify({ success: true, action: "none", reactions: msg.reactions }, null, 2));
|
|
30717
|
+
return;
|
|
30718
|
+
}
|
|
30719
|
+
console.log(dim("You have no reaction on that message."));
|
|
30720
|
+
return;
|
|
30721
|
+
}
|
|
30722
|
+
await requireApproval({
|
|
30723
|
+
free: "removes your reaction from the chat",
|
|
30724
|
+
summary: `Remove your ${mine} reaction from "${previewText(msg)}"`,
|
|
30725
|
+
yes: opts.yes,
|
|
30726
|
+
commandExample: `amiko chat unreact ${JSON.stringify(messageId)}`
|
|
30727
|
+
});
|
|
29737
30728
|
let data;
|
|
29738
30729
|
try {
|
|
29739
|
-
data = await amikoWebFetch(auth,
|
|
29740
|
-
query: {
|
|
29741
|
-
limit: opts.limit,
|
|
29742
|
-
...opts.archived ? { archived: "all" } : {}
|
|
29743
|
-
},
|
|
29744
|
-
timeoutMs: 20000
|
|
29745
|
-
});
|
|
30730
|
+
data = await amikoWebFetch(auth, `/api/messages/${encodeURIComponent(messageId)}/reactions`, { method: "POST", body: { emoji: mine }, timeoutMs: 20000 });
|
|
29746
30731
|
} catch (e5) {
|
|
29747
|
-
console.error(error(e5 instanceof Error ? e5.message : "Failed to
|
|
30732
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to remove reaction"));
|
|
29748
30733
|
process.exit(1);
|
|
29749
30734
|
}
|
|
29750
30735
|
if (opts.raw) {
|
|
29751
30736
|
console.log(JSON.stringify(data, null, 2));
|
|
29752
30737
|
return;
|
|
29753
30738
|
}
|
|
29754
|
-
const
|
|
29755
|
-
|
|
29756
|
-
|
|
30739
|
+
const what = data.action === "removed" ? `Removed your ${mine} reaction` : data.action === "updated" ? `Reaction changed to ${mine}` : `Reacted ${mine}`;
|
|
30740
|
+
console.log(success(what));
|
|
30741
|
+
});
|
|
30742
|
+
chat.command("reactions <messageId>").description("Show the reactions on a message. Get the message id from `amiko chat read`.").option("--raw", "Output raw JSON").action(async (messageId, opts) => {
|
|
30743
|
+
const auth = authOrExit();
|
|
30744
|
+
let msg;
|
|
30745
|
+
try {
|
|
30746
|
+
msg = await fetchMessage(auth, messageId);
|
|
30747
|
+
} catch (e5) {
|
|
30748
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load that message"));
|
|
30749
|
+
process.exit(1);
|
|
30750
|
+
}
|
|
30751
|
+
if (opts.raw) {
|
|
30752
|
+
console.log(JSON.stringify({ reactions: msg.reactions }, null, 2));
|
|
29757
30753
|
return;
|
|
29758
30754
|
}
|
|
29759
|
-
|
|
30755
|
+
if (msg.reactions.length === 0) {
|
|
30756
|
+
console.log(dim("No reactions yet."));
|
|
30757
|
+
return;
|
|
30758
|
+
}
|
|
30759
|
+
const mine = ownReaction(msg.reactions, auth.userId);
|
|
30760
|
+
console.log(heading(`Reactions (${msg.reactions.length})
|
|
29760
30761
|
`));
|
|
29761
|
-
for (const
|
|
29762
|
-
const
|
|
29763
|
-
|
|
29764
|
-
|
|
29765
|
-
|
|
29766
|
-
|
|
29767
|
-
|
|
30762
|
+
for (const [emoji3, count] of groupReactions(msg.reactions)) {
|
|
30763
|
+
const you = emoji3 === mine ? dim(" (includes you)") : "";
|
|
30764
|
+
console.log(label(emoji3, `×${count}`) + you);
|
|
30765
|
+
}
|
|
30766
|
+
});
|
|
30767
|
+
chat.command("receipts <messageId>").description("Show who read your message and when (sender-only). Get the message id from `amiko chat read`.").option("--raw", "Output raw JSON").action(async (messageId, opts) => {
|
|
30768
|
+
const auth = authOrExit();
|
|
30769
|
+
let msg;
|
|
30770
|
+
try {
|
|
30771
|
+
msg = await fetchMessage(auth, messageId);
|
|
30772
|
+
} catch (e5) {
|
|
30773
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load that message"));
|
|
30774
|
+
process.exit(1);
|
|
30775
|
+
}
|
|
30776
|
+
let data;
|
|
30777
|
+
try {
|
|
30778
|
+
data = await amikoWebFetch(auth, `/api/messages/${encodeURIComponent(messageId)}/receipts`, { timeoutMs: 20000 });
|
|
30779
|
+
} catch (e5) {
|
|
30780
|
+
const status = e5.status;
|
|
30781
|
+
console.error(error(status === 403 ? "Read receipts are only available for messages you sent (as the owner)." : e5 instanceof Error ? e5.message : "Failed to load receipts"));
|
|
30782
|
+
process.exit(1);
|
|
30783
|
+
}
|
|
30784
|
+
const reads = data.reads ?? [];
|
|
30785
|
+
const delivered = data.delivered_to ?? [];
|
|
30786
|
+
if (opts.raw) {
|
|
30787
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30788
|
+
return;
|
|
30789
|
+
}
|
|
30790
|
+
const names = new Map;
|
|
30791
|
+
if (msg.conversation_id) {
|
|
30792
|
+
try {
|
|
30793
|
+
const conv = await fetchConversationById(auth, msg.conversation_id);
|
|
30794
|
+
for (const p of conv.participants ?? []) {
|
|
30795
|
+
if (p.participant_type !== "user")
|
|
30796
|
+
continue;
|
|
30797
|
+
const id = normalizeUserId(p.participant_id);
|
|
30798
|
+
names.set(id, p.user?.name ?? p.user?.handle ?? id.slice(0, 8));
|
|
30799
|
+
}
|
|
30800
|
+
} catch {}
|
|
30801
|
+
}
|
|
30802
|
+
const nameFor = (userId) => {
|
|
30803
|
+
const id = normalizeUserId(userId);
|
|
30804
|
+
return names.get(id) ?? id.slice(0, 8);
|
|
30805
|
+
};
|
|
30806
|
+
const fmt = (iso) => iso.slice(0, 19).replace("T", " ");
|
|
30807
|
+
console.log(label("Message", previewText(msg)));
|
|
30808
|
+
console.log(label("Sent", fmt(data.sent_at)));
|
|
30809
|
+
console.log("");
|
|
30810
|
+
console.log(heading(`Read by (${reads.length})`));
|
|
30811
|
+
if (reads.length === 0) {
|
|
30812
|
+
console.log(dim(" No one has read this yet."));
|
|
30813
|
+
} else {
|
|
30814
|
+
for (const r of reads) {
|
|
30815
|
+
const when = r.approximate ? dim("read (exact time unknown)") : fmt(r.read_at);
|
|
30816
|
+
console.log(` ${label(nameFor(r.user_id), when)}`);
|
|
30817
|
+
}
|
|
30818
|
+
}
|
|
30819
|
+
if (delivered.length > 0) {
|
|
30820
|
+
console.log("");
|
|
30821
|
+
console.log(heading(`Delivered to (${delivered.length})`));
|
|
30822
|
+
for (const userId of delivered) {
|
|
30823
|
+
console.log(` ${nameFor(userId)}`);
|
|
29768
30824
|
}
|
|
29769
|
-
if (unread)
|
|
29770
|
-
console.log(dim(` ${unread.trim()}`));
|
|
29771
30825
|
}
|
|
29772
30826
|
});
|
|
29773
|
-
chat.command("
|
|
30827
|
+
chat.command("pin <messageId>").description("Pin a message for everyone in the conversation. Get the message id from `amiko chat read`. Groups require the admin role; the chat sees a pinned-message announcement.").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (messageId, opts) => {
|
|
30828
|
+
const auth = authOrExit();
|
|
30829
|
+
let msg;
|
|
30830
|
+
try {
|
|
30831
|
+
msg = await fetchMessage(auth, messageId);
|
|
30832
|
+
} catch (e5) {
|
|
30833
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load that message"));
|
|
30834
|
+
process.exit(1);
|
|
30835
|
+
}
|
|
30836
|
+
await requireApproval({
|
|
30837
|
+
free: "pins the message for everyone — the chat sees a pinned-message announcement",
|
|
30838
|
+
summary: `Pin "${previewText(msg)}"`,
|
|
30839
|
+
yes: opts.yes,
|
|
30840
|
+
commandExample: `amiko chat pin ${JSON.stringify(messageId)}`
|
|
30841
|
+
});
|
|
30842
|
+
let data;
|
|
30843
|
+
try {
|
|
30844
|
+
data = await amikoWebFetch(auth, `/api/messages/${encodeURIComponent(messageId)}/pin`, { method: "POST", timeoutMs: 20000 });
|
|
30845
|
+
} catch (e5) {
|
|
30846
|
+
console.error(error(pinErrorMessage(e5, "Failed to pin the message")));
|
|
30847
|
+
process.exit(1);
|
|
30848
|
+
}
|
|
30849
|
+
if (opts.raw) {
|
|
30850
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30851
|
+
return;
|
|
30852
|
+
}
|
|
30853
|
+
console.log(success(`Pinned "${previewText(msg)}"`));
|
|
30854
|
+
const total = data.pins?.length;
|
|
30855
|
+
if (typeof total === "number") {
|
|
30856
|
+
console.log(dim(` ${total}/${PIN_LIMIT} pinned in this conversation — see them with \`amiko chat pinned\`.`));
|
|
30857
|
+
}
|
|
30858
|
+
});
|
|
30859
|
+
chat.command("unpin <messageId>").description("Unpin a message — removes it from the conversation's pinned list for everyone. Get the message id from `amiko chat read` or `amiko chat pinned`.").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (messageId, opts) => {
|
|
30860
|
+
const auth = authOrExit();
|
|
30861
|
+
let msg;
|
|
30862
|
+
try {
|
|
30863
|
+
msg = await fetchMessage(auth, messageId);
|
|
30864
|
+
} catch (e5) {
|
|
30865
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load that message"));
|
|
30866
|
+
process.exit(1);
|
|
30867
|
+
}
|
|
30868
|
+
await confirmDestructive({
|
|
30869
|
+
action: `Unpin "${previewText(msg)}" for everyone in the conversation`,
|
|
30870
|
+
yes: opts.yes,
|
|
30871
|
+
commandExample: `amiko chat unpin ${JSON.stringify(messageId)}`
|
|
30872
|
+
});
|
|
30873
|
+
let data;
|
|
30874
|
+
try {
|
|
30875
|
+
data = await amikoWebFetch(auth, `/api/messages/${encodeURIComponent(messageId)}/pin`, { method: "DELETE", timeoutMs: 20000 });
|
|
30876
|
+
} catch (e5) {
|
|
30877
|
+
console.error(error(pinErrorMessage(e5, "Failed to unpin the message")));
|
|
30878
|
+
process.exit(1);
|
|
30879
|
+
}
|
|
30880
|
+
if (opts.raw) {
|
|
30881
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30882
|
+
return;
|
|
30883
|
+
}
|
|
30884
|
+
console.log(success(`Unpinned "${previewText(msg)}"`));
|
|
30885
|
+
const remaining = data.pins?.length;
|
|
30886
|
+
if (typeof remaining === "number" && remaining > 0) {
|
|
30887
|
+
console.log(dim(` ${remaining} still pinned in this conversation.`));
|
|
30888
|
+
}
|
|
30889
|
+
});
|
|
30890
|
+
chat.command("pinned <target>").description("List a conversation's pinned messages, oldest first (conversation id, user id, @handle, or name)").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
29774
30891
|
const auth = authOrExit();
|
|
29775
30892
|
let convId;
|
|
29776
30893
|
try {
|
|
@@ -29790,95 +30907,73 @@ function registerChatCommand(chat) {
|
|
|
29790
30907
|
}
|
|
29791
30908
|
let data;
|
|
29792
30909
|
try {
|
|
29793
|
-
data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(convId)}/messages`, {
|
|
30910
|
+
data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(convId)}/pinned-messages`, { timeoutMs: 20000 });
|
|
29794
30911
|
} catch (e5) {
|
|
29795
|
-
console.error(error(e5 instanceof Error ? e5.message : "Failed to
|
|
30912
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to load pinned messages"));
|
|
29796
30913
|
process.exit(1);
|
|
29797
30914
|
}
|
|
29798
30915
|
if (opts.raw) {
|
|
29799
30916
|
console.log(JSON.stringify(data, null, 2));
|
|
29800
30917
|
return;
|
|
29801
30918
|
}
|
|
29802
|
-
const
|
|
29803
|
-
if (
|
|
29804
|
-
console.log(dim("No messages."));
|
|
30919
|
+
const pins = data.pins ?? [];
|
|
30920
|
+
if (pins.length === 0) {
|
|
30921
|
+
console.log(dim("No pinned messages."));
|
|
29805
30922
|
return;
|
|
29806
30923
|
}
|
|
29807
|
-
console.log(heading(`
|
|
30924
|
+
console.log(heading(`Pinned messages (${pins.length})
|
|
29808
30925
|
`));
|
|
29809
|
-
for (const
|
|
29810
|
-
const
|
|
30926
|
+
for (const pin of pins) {
|
|
30927
|
+
const m = pin.message;
|
|
30928
|
+
const who = m.name ?? (m.sender_id ? m.sender_id.slice(0, 8) : "?");
|
|
29811
30929
|
const when = m.created_at ? m.created_at.slice(0, 19).replace("T", " ") : "";
|
|
29812
|
-
console.log(label(who, m.content));
|
|
29813
|
-
|
|
29814
|
-
|
|
30930
|
+
console.log(label(who, mentionMarkupToPlainText(m.content ?? "")));
|
|
30931
|
+
const meta3 = [when, `id ${m.id}`].filter(Boolean).join(" ");
|
|
30932
|
+
console.log(dim(` ${meta3}`));
|
|
30933
|
+
if (pin.pinned_by_name) {
|
|
30934
|
+
console.log(dim(` pinned by ${pin.pinned_by_name}`));
|
|
30935
|
+
}
|
|
29815
30936
|
}
|
|
29816
30937
|
});
|
|
29817
|
-
chat.command("
|
|
29818
|
-
|
|
29819
|
-
|
|
29820
|
-
console.error(error("Attach either --image or --audio, not both (one media block per message)."));
|
|
30938
|
+
chat.command("mark-read").description("Mark ALL conversations as read (requires --all). Clears every unread badge on the owner's devices; senders see read ticks.").option("--all", "Mark every conversation as read").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (opts) => {
|
|
30939
|
+
if (!opts.all) {
|
|
30940
|
+
console.error(error("Pass --all to mark every conversation as read"));
|
|
29821
30941
|
process.exit(1);
|
|
29822
30942
|
}
|
|
29823
|
-
|
|
29824
|
-
|
|
29825
|
-
|
|
29826
|
-
|
|
29827
|
-
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
29828
|
-
process.exit(1);
|
|
29829
|
-
}
|
|
29830
|
-
const media = opts.image ? `image ${opts.image}` : opts.audio ? `audio ${opts.audio}` : "";
|
|
29831
|
-
await requireApproval({
|
|
29832
|
-
free: "sends a message as you (the owner) — the recipients see it",
|
|
29833
|
-
summary: `Send to ${resolved.label}${media ? ` [+${media}]` : ""}: ${message.slice(0, 80)}${message.length > 80 ? "…" : ""}`,
|
|
30943
|
+
const auth = authOrExit();
|
|
30944
|
+
await confirmDestructive({
|
|
30945
|
+
action: "Mark ALL of the owner's conversations as read",
|
|
30946
|
+
detail: "Clears every unread badge on web and mobile; senders see read ticks on their messages. This cannot be undone.",
|
|
29834
30947
|
yes: opts.yes,
|
|
29835
|
-
commandExample:
|
|
29836
|
-
});
|
|
29837
|
-
let convId;
|
|
29838
|
-
try {
|
|
29839
|
-
convId = resolved.kind === "user" ? await findOrCreateDm(auth, resolved.userId) : resolved.id;
|
|
29840
|
-
} catch (e5) {
|
|
29841
|
-
console.error(error(e5 instanceof Error ? e5.message : "Could not open the conversation"));
|
|
29842
|
-
process.exit(1);
|
|
29843
|
-
}
|
|
29844
|
-
const messagesPath = `/api/conversations/${encodeURIComponent(convId)}/messages`;
|
|
29845
|
-
const post = (body) => amikoWebFetch(auth, messagesPath, {
|
|
29846
|
-
method: "POST",
|
|
29847
|
-
body,
|
|
29848
|
-
timeoutMs: 20000
|
|
30948
|
+
commandExample: "amiko chat mark-read --all"
|
|
29849
30949
|
});
|
|
29850
30950
|
let data;
|
|
29851
30951
|
try {
|
|
29852
|
-
|
|
29853
|
-
|
|
29854
|
-
|
|
29855
|
-
}
|
|
29856
|
-
if (HTTPS_RE.test(opts.audio)) {
|
|
29857
|
-
data = await post({
|
|
29858
|
-
content: message,
|
|
29859
|
-
message_type: "voice",
|
|
29860
|
-
metadata: { audioUrl: opts.audio }
|
|
29861
|
-
});
|
|
29862
|
-
} else {
|
|
29863
|
-
data = await post({ content: message });
|
|
29864
|
-
await uploadChatVoice(auth, convId, opts.audio);
|
|
29865
|
-
}
|
|
29866
|
-
} else {
|
|
29867
|
-
data = await post({ content: message });
|
|
29868
|
-
}
|
|
30952
|
+
data = await amikoWebFetch(auth, "/api/conversations/read-all", {
|
|
30953
|
+
method: "POST",
|
|
30954
|
+
timeoutMs: 20000
|
|
30955
|
+
});
|
|
29869
30956
|
} catch (e5) {
|
|
29870
|
-
console.error(error(e5 instanceof Error ? e5.message : "
|
|
30957
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to mark all as read"));
|
|
29871
30958
|
process.exit(1);
|
|
29872
30959
|
}
|
|
29873
30960
|
if (opts.raw) {
|
|
29874
30961
|
console.log(JSON.stringify(data, null, 2));
|
|
29875
30962
|
return;
|
|
29876
30963
|
}
|
|
29877
|
-
|
|
29878
|
-
|
|
30964
|
+
console.log(success("All conversations marked as read."));
|
|
30965
|
+
const cleared = data.cleared_notifications;
|
|
30966
|
+
if (typeof cleared === "number" && cleared > 0) {
|
|
30967
|
+
console.log(dim(` ${cleared} chat notification${cleared === 1 ? "" : "s"} cleared`));
|
|
30968
|
+
} else if ((data.unread_conversation_ids ?? []).length === 0) {
|
|
30969
|
+
console.log(dim(" Nothing was unread."));
|
|
30970
|
+
}
|
|
29879
30971
|
});
|
|
29880
|
-
|
|
30972
|
+
registerChatGifsCommand(chat);
|
|
30973
|
+
const group = chat.command("group").description("Group chats — create, list, info, rename, add/remove members, @all permission, leave");
|
|
29881
30974
|
registerChatGroupCommand(group);
|
|
30975
|
+
const lists = chat.command("lists").description("Your chat lists (private folders of conversations) — run bare to see them; create, rename, add/remove conversations, delete");
|
|
30976
|
+
registerChatListsCommand(lists);
|
|
29882
30977
|
}
|
|
29883
30978
|
|
|
29884
30979
|
// src/commands/card.ts
|
|
@@ -30096,7 +31191,7 @@ function registerCardCommand(card) {
|
|
|
30096
31191
|
// src/commands/amazon.ts
|
|
30097
31192
|
var AMAZON_SEARCH_USD = 0.01;
|
|
30098
31193
|
function registerAmazonCommand(program2) {
|
|
30099
|
-
const cmd = program2.command("amazon").description("Search and buy products on Amazon —
|
|
31194
|
+
const cmd = program2.command("amazon").description("Search and buy products on Amazon — pay in AMIKO, USDC, or USDT");
|
|
30100
31195
|
cmd.command("search <query>").description("Search Amazon products — $0.01, paid in AMIKO, USDC, or USDT").option("--limit <n>", "Number of results", "5").option("--wallet <address>", "Wallet to pay from (defaults to your Solana wallet — run `amiko wallets create --chain solana` if you don't have one)").option("--token <token>", "Payment token (AMIKO, USDC, USDT)", "AMIKO").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (query, opts) => {
|
|
30101
31196
|
let payToken;
|
|
30102
31197
|
try {
|
|
@@ -30313,19 +31408,33 @@ async function fetchWithTimeout2(url2, init = {}, timeoutMs) {
|
|
|
30313
31408
|
}
|
|
30314
31409
|
}
|
|
30315
31410
|
function registerServicesCommand(program2) {
|
|
30316
|
-
const svc = program2.command("service").alias("svc").description("Call any MPP service —
|
|
30317
|
-
svc.command("tts <voiceId> <text>").description("Text-to-speech
|
|
31411
|
+
const svc = program2.command("service").alias("svc").description("Call any MPP service — pay in AMIKO, USDC, or USDT");
|
|
31412
|
+
svc.command("tts <voiceId> <text>").description("Text-to-speech via mpp /v1/tts (pay-per-call)").option("--provider <name>", "elevenlabs (default) | minimax").option("--model <model>", "MiniMax speech-* model when provider=minimax").option("--raw", "Output raw response").option("--wallet <address>", "Wallet to pay from (defaults to your Solana wallet — run `amiko wallets create --chain solana` if none)").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (voiceId, text, opts) => {
|
|
31413
|
+
const provider = typeof opts.provider === "string" ? opts.provider.trim().toLowerCase() : "";
|
|
31414
|
+
const body = { text };
|
|
31415
|
+
if (provider === "minimax") {
|
|
31416
|
+
body.provider = "minimax";
|
|
31417
|
+
if (opts.model)
|
|
31418
|
+
body.model_id = opts.model;
|
|
31419
|
+
else
|
|
31420
|
+
body.model_id = "speech-2.8-turbo";
|
|
31421
|
+
} else if (provider && provider !== "elevenlabs") {
|
|
31422
|
+
console.error(error("--provider must be minimax or elevenlabs"));
|
|
31423
|
+
process.exit(1);
|
|
31424
|
+
}
|
|
31425
|
+
const providerArgs = provider ? ` --provider ${provider}` : "";
|
|
31426
|
+
const modelArgs = provider === "minimax" ? ` --model ${JSON.stringify(typeof opts.model === "string" && opts.model ? opts.model : "speech-2.8-turbo")}` : "";
|
|
30318
31427
|
await requireApproval({
|
|
30319
|
-
cost: "
|
|
30320
|
-
summary: `Text-to-speech (voice ${voiceId}): "${text.slice(0, 60)}${text.length > 60 ? "…" : ""}"`,
|
|
31428
|
+
cost: "variable (see mpp pricing)",
|
|
31429
|
+
summary: `Text-to-speech (voice ${voiceId}${provider ? `, ${provider}` : ""}): "${text.slice(0, 60)}${text.length > 60 ? "…" : ""}"`,
|
|
30321
31430
|
yes: opts.yes,
|
|
30322
|
-
commandExample: `amiko markets service tts ${voiceId} ${JSON.stringify(text)}`
|
|
31431
|
+
commandExample: `amiko markets service tts ${voiceId} ${JSON.stringify(text)}${providerArgs}${modelArgs}`
|
|
30323
31432
|
});
|
|
30324
31433
|
await callService({
|
|
30325
31434
|
name: "Text-to-Speech",
|
|
30326
31435
|
method: "POST",
|
|
30327
31436
|
path: `/v1/tts/${encodeURIComponent(voiceId)}`,
|
|
30328
|
-
body
|
|
31437
|
+
body,
|
|
30329
31438
|
amiko: "1",
|
|
30330
31439
|
opts
|
|
30331
31440
|
});
|
|
@@ -30537,7 +31646,7 @@ function registerAccountsCommand(program2) {
|
|
|
30537
31646
|
}
|
|
30538
31647
|
}, { json: opts.json });
|
|
30539
31648
|
});
|
|
30540
|
-
program2.command("info").description("Get twin info (name, description, voice, avatar)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
|
|
31649
|
+
program2.command("info").description("Get twin info (name, description, voice ids, avatar)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
|
|
30541
31650
|
const config2 = loadConfig();
|
|
30542
31651
|
const auth = resolveAuth();
|
|
30543
31652
|
let twinId;
|
|
@@ -30561,13 +31670,24 @@ function registerAccountsCommand(program2) {
|
|
|
30561
31670
|
console.log(label("Description", t.description));
|
|
30562
31671
|
console.log(label("Public", t.is_public ? "yes" : "no"));
|
|
30563
31672
|
if (t.voice_status)
|
|
30564
|
-
console.log(label("Voice", t.voice_status));
|
|
31673
|
+
console.log(label("Voice status", t.voice_status));
|
|
31674
|
+
if (t.voice_id)
|
|
31675
|
+
console.log(label("ElevenLabs voice_id", t.voice_id));
|
|
31676
|
+
else
|
|
31677
|
+
console.log(label("ElevenLabs voice_id", dim("(none)")));
|
|
31678
|
+
if (t.minimax_voice_id)
|
|
31679
|
+
console.log(label("MiniMax voice_id", t.minimax_voice_id));
|
|
31680
|
+
else
|
|
31681
|
+
console.log(label("MiniMax voice_id", dim("(none — clone with amiko voice clone)")));
|
|
30565
31682
|
if (t.avatar_url)
|
|
30566
31683
|
console.log(label("Avatar", t.avatar_url));
|
|
30567
31684
|
if (t.shipped_at)
|
|
30568
31685
|
console.log(label("Shipped", t.shipped_at));
|
|
30569
31686
|
if (t.updated_at)
|
|
30570
31687
|
console.log(dim(`Updated ${t.updated_at}`));
|
|
31688
|
+
console.log("");
|
|
31689
|
+
console.log(dim('MiniMax TTS: amiko create tts "…" --provider minimax --voice <MiniMax voice_id> --yes'));
|
|
31690
|
+
console.log(dim('ElevenLabs TTS: amiko create tts "…" --provider elevenlabs --voice <ElevenLabs voice_id> --yes'));
|
|
30571
31691
|
}, { json: opts.json });
|
|
30572
31692
|
} catch (e5) {
|
|
30573
31693
|
spinner?.stop();
|
|
@@ -30649,6 +31769,9 @@ function registerTwinCommand(program2) {
|
|
|
30649
31769
|
// src/commands/drive.ts
|
|
30650
31770
|
import { writeFile as writeFile2 } from "node:fs/promises";
|
|
30651
31771
|
import { basename as basename2, resolve as resolvePath } from "node:path";
|
|
31772
|
+
var DIRECT_UPLOAD_MAX_BYTES = 4 * 1024 * 1024;
|
|
31773
|
+
var MAX_DRIVE_UPLOAD_BYTES = 200 * 1024 * 1024;
|
|
31774
|
+
var PRESIGNED_PUT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
30652
31775
|
function shareBase(auth) {
|
|
30653
31776
|
return (auth.platformUrl ?? "https://platform.heyamiko.com").replace(/\/$/, "");
|
|
30654
31777
|
}
|
|
@@ -30783,26 +31906,56 @@ Search hits filename, title, description, and parsed RAG content (once processed
|
|
|
30783
31906
|
try {
|
|
30784
31907
|
const file2 = await resolveFileInput(source, {
|
|
30785
31908
|
stdin: opts.stdin,
|
|
30786
|
-
hintName: opts.name
|
|
31909
|
+
hintName: opts.name,
|
|
31910
|
+
maxBytes: MAX_DRIVE_UPLOAD_BYTES
|
|
30787
31911
|
});
|
|
30788
31912
|
if (spinner)
|
|
30789
|
-
spinner.text = `Uploading ${file2.name}...`;
|
|
30790
|
-
|
|
30791
|
-
|
|
30792
|
-
|
|
30793
|
-
|
|
30794
|
-
|
|
30795
|
-
|
|
30796
|
-
|
|
30797
|
-
|
|
31913
|
+
spinner.text = `Uploading ${file2.name} (${fmtBytes(file2.size)})...`;
|
|
31914
|
+
let uploadedUrl;
|
|
31915
|
+
let uploadedName;
|
|
31916
|
+
let uploadedType;
|
|
31917
|
+
if (file2.size > DIRECT_UPLOAD_MAX_BYTES) {
|
|
31918
|
+
const pres = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/presigned-url`, {
|
|
31919
|
+
method: "POST",
|
|
31920
|
+
body: { filename: file2.name }
|
|
31921
|
+
});
|
|
31922
|
+
if (!pres.uploadUrl || !pres.publicUrl) {
|
|
31923
|
+
throw new Error(`Presigned-URL response missing fields: ${JSON.stringify(pres)}`);
|
|
31924
|
+
}
|
|
31925
|
+
const putRes = await fetch(pres.uploadUrl, {
|
|
31926
|
+
method: "PUT",
|
|
31927
|
+
headers: { "Content-Type": file2.mime },
|
|
31928
|
+
body: new Uint8Array(file2.buffer),
|
|
31929
|
+
signal: AbortSignal.timeout(PRESIGNED_PUT_TIMEOUT_MS)
|
|
31930
|
+
});
|
|
31931
|
+
if (!putRes.ok) {
|
|
31932
|
+
const detail = await putRes.text().catch(() => "");
|
|
31933
|
+
throw new Error(`Direct storage upload failed: ${putRes.status} ${putRes.statusText}${detail ? ` — ${detail.slice(0, 200)}` : ""}`);
|
|
31934
|
+
}
|
|
31935
|
+
uploadedUrl = pres.publicUrl;
|
|
31936
|
+
uploadedName = file2.name;
|
|
31937
|
+
uploadedType = file2.mime;
|
|
31938
|
+
} else {
|
|
31939
|
+
const form = new FormData;
|
|
31940
|
+
form.set("file", new Blob([new Uint8Array(file2.buffer)], { type: file2.mime }), file2.name);
|
|
31941
|
+
const upload = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/upload`, {
|
|
31942
|
+
method: "POST",
|
|
31943
|
+
multipart: form
|
|
31944
|
+
});
|
|
31945
|
+
if (!upload.url || !upload.filename || !upload.fileType) {
|
|
31946
|
+
throw new Error(`Upload response missing required fields: ${JSON.stringify(upload)}`);
|
|
31947
|
+
}
|
|
31948
|
+
uploadedUrl = upload.url;
|
|
31949
|
+
uploadedName = upload.filename;
|
|
31950
|
+
uploadedType = upload.fileType;
|
|
30798
31951
|
}
|
|
30799
31952
|
if (spinner)
|
|
30800
|
-
spinner.text = `Registering ${
|
|
30801
|
-
const title = opts.title ||
|
|
31953
|
+
spinner.text = `Registering ${uploadedName}...`;
|
|
31954
|
+
const title = opts.title || uploadedName.replace(/\.[^.]+$/, "");
|
|
30802
31955
|
const body = {
|
|
30803
|
-
filename:
|
|
30804
|
-
fileUrl:
|
|
30805
|
-
fileType:
|
|
31956
|
+
filename: uploadedName,
|
|
31957
|
+
fileUrl: uploadedUrl,
|
|
31958
|
+
fileType: uploadedType,
|
|
30806
31959
|
title,
|
|
30807
31960
|
doc_type: opts.docType ?? "other"
|
|
30808
31961
|
};
|
|
@@ -30818,10 +31971,8 @@ Search hits filename, title, description, and parsed RAG content (once processed
|
|
|
30818
31971
|
console.log(label("Title", d.title || "-"));
|
|
30819
31972
|
if (d.folder_id)
|
|
30820
31973
|
console.log(label("Folder", d.folder_id));
|
|
30821
|
-
|
|
30822
|
-
|
|
30823
|
-
if (upload.fileType)
|
|
30824
|
-
console.log(label("Type", upload.fileType));
|
|
31974
|
+
console.log(label("Size", fmtBytes(file2.size)));
|
|
31975
|
+
console.log(label("Type", uploadedType));
|
|
30825
31976
|
if (d.job_id) {
|
|
30826
31977
|
console.log(label("RAG job", d.job_id));
|
|
30827
31978
|
console.log(dim("Processing runs in the background. Use `amiko drive status <id>` to check."));
|
|
@@ -31173,8 +32324,18 @@ function twinFromFlag2(opts) {
|
|
|
31173
32324
|
throw e5;
|
|
31174
32325
|
}
|
|
31175
32326
|
}
|
|
32327
|
+
function parseProvider(raw, fallback2) {
|
|
32328
|
+
if (raw == null || raw.trim() === "")
|
|
32329
|
+
return fallback2;
|
|
32330
|
+
const p = raw.trim().toLowerCase();
|
|
32331
|
+
if (p !== "minimax" && p !== "elevenlabs") {
|
|
32332
|
+
console.error(error("--provider must be minimax or elevenlabs"));
|
|
32333
|
+
process.exit(1);
|
|
32334
|
+
}
|
|
32335
|
+
return p;
|
|
32336
|
+
}
|
|
31176
32337
|
function registerVoiceCommand(program2) {
|
|
31177
|
-
program2.command("design").description("Design a voice from a text description").requiredOption("--description <text>", "Voice description (min 20 chars)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
|
|
32338
|
+
program2.command("design").description("Design a voice from a text description (ElevenLabs)").requiredOption("--description <text>", "Voice description (min 20 chars)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
|
|
31178
32339
|
if (opts.description.trim().length < 20) {
|
|
31179
32340
|
console.error(error("--description must be at least 20 characters"));
|
|
31180
32341
|
process.exit(1);
|
|
@@ -31202,7 +32363,7 @@ function registerVoiceCommand(program2) {
|
|
|
31202
32363
|
process.exit(1);
|
|
31203
32364
|
}
|
|
31204
32365
|
});
|
|
31205
|
-
program2.command("create").description("Create a voice from a previously designed preview").requiredOption("--sample <id>", "generated_voice_id from voice design").option("--description <text>", "Optional voice description").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
|
|
32366
|
+
program2.command("create").description("Create a voice from a previously designed preview (ElevenLabs)").requiredOption("--sample <id>", "generated_voice_id from voice design").option("--description <text>", "Optional voice description").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
|
|
31206
32367
|
const { twinId, auth } = twinFromFlag2(opts);
|
|
31207
32368
|
const spinner = opts.json ? null : ora("Creating voice...").start();
|
|
31208
32369
|
try {
|
|
@@ -31228,7 +32389,8 @@ function registerVoiceCommand(program2) {
|
|
|
31228
32389
|
process.exit(1);
|
|
31229
32390
|
}
|
|
31230
32391
|
});
|
|
31231
|
-
program2.command("clone <source>").description("Clone a voice from an audio file (path | https URL | -)").option("--twin <idOrName>", "Target twin id or name").option("--name <filename>", "Override filename").option("--stdin", "Read from stdin (source must be -)").option("--json", "Output as JSON").action(async (source, opts) => {
|
|
32392
|
+
program2.command("clone <source>").description("Clone a voice from an audio file (path | https URL | -). Default provider: minimax").option("--provider <name>", "minimax (default, mpp rapid clone) | elevenlabs (platform IVC)", "minimax").option("--voice-id <id>", "MiniMax: optional custom voice_id (8–256 chars)").option("--model <model>", "MiniMax preview TTS model", "speech-2.8-turbo").option("--preview-text <text>", "MiniMax: preview line (empty string skips preview)").option("--twin <idOrName>", "Target twin id or name").option("--name <filename>", "Override filename").option("--stdin", "Read from stdin (source must be -)").option("--json", "Output as JSON").action(async (source, opts) => {
|
|
32393
|
+
const provider = parseProvider(opts.provider, "minimax");
|
|
31232
32394
|
const { twinId, auth } = twinFromFlag2(opts);
|
|
31233
32395
|
const spinner = opts.json ? null : ora("Reading audio...").start();
|
|
31234
32396
|
try {
|
|
@@ -31238,17 +32400,30 @@ function registerVoiceCommand(program2) {
|
|
|
31238
32400
|
hintMime: "audio/mpeg"
|
|
31239
32401
|
});
|
|
31240
32402
|
if (spinner)
|
|
31241
|
-
spinner.text = `Cloning voice from ${file2.name}...`;
|
|
32403
|
+
spinner.text = `Cloning voice (${provider}) from ${file2.name}...`;
|
|
31242
32404
|
const form = new FormData;
|
|
31243
32405
|
form.set("audio", new Blob([new Uint8Array(file2.buffer)], { type: file2.mime }), file2.name);
|
|
32406
|
+
form.set("provider", provider);
|
|
32407
|
+
if (provider === "minimax") {
|
|
32408
|
+
if (opts.voiceId)
|
|
32409
|
+
form.set("voice_id", opts.voiceId);
|
|
32410
|
+
if (opts.model)
|
|
32411
|
+
form.set("model_id", opts.model);
|
|
32412
|
+
if (opts.previewText !== undefined)
|
|
32413
|
+
form.set("text", opts.previewText);
|
|
32414
|
+
}
|
|
31244
32415
|
const result = await amikoWebFetch(auth, `/api/agents/${twinId}/voice/clone`, { method: "POST", multipart: form, timeoutMs: 180000 });
|
|
31245
32416
|
spinner?.stop();
|
|
31246
32417
|
renderOutput(result, (r) => {
|
|
31247
|
-
console.log(success(
|
|
32418
|
+
console.log(success(`Voice cloned (${String(r.provider ?? provider)})`));
|
|
31248
32419
|
if (r.voice_id)
|
|
31249
32420
|
console.log(label("Voice ID", String(r.voice_id)));
|
|
31250
32421
|
if (r.voice_status)
|
|
31251
32422
|
console.log(label("Status", String(r.voice_status)));
|
|
32423
|
+
if (r.demo_audio)
|
|
32424
|
+
console.log(label("Demo", String(r.demo_audio)));
|
|
32425
|
+
console.log("");
|
|
32426
|
+
console.log(dim(`Synthesize: amiko create tts "Hello" --provider ${provider} --voice ${String(r.voice_id ?? "<id>")} --yes`));
|
|
31252
32427
|
}, { json: opts.json });
|
|
31253
32428
|
} catch (e5) {
|
|
31254
32429
|
spinner?.stop();
|
|
@@ -31451,33 +32626,223 @@ async function mppFriendReportGenerate(auth, body, opts = {}) {
|
|
|
31451
32626
|
log("Generating report...");
|
|
31452
32627
|
const url2 = `${mppBase2}/v1/friend-reports/generate`;
|
|
31453
32628
|
try {
|
|
31454
|
-
const res = await fetch(url2, {
|
|
31455
|
-
method: "POST",
|
|
31456
|
-
headers: {
|
|
31457
|
-
"Content-Type": "application/json",
|
|
31458
|
-
"X-Payment-Tx": txHash
|
|
31459
|
-
},
|
|
31460
|
-
body: JSON.stringify(normalizedBody)
|
|
32629
|
+
const res = await fetch(url2, {
|
|
32630
|
+
method: "POST",
|
|
32631
|
+
headers: {
|
|
32632
|
+
"Content-Type": "application/json",
|
|
32633
|
+
"X-Payment-Tx": txHash
|
|
32634
|
+
},
|
|
32635
|
+
body: JSON.stringify(normalizedBody)
|
|
32636
|
+
});
|
|
32637
|
+
const data = await res.json().catch(() => ({}));
|
|
32638
|
+
if (!res.ok) {
|
|
32639
|
+
return {
|
|
32640
|
+
ok: false,
|
|
32641
|
+
status: res.status,
|
|
32642
|
+
error: (typeof data.error === "string" ? data.error : null) ?? `mpp-service returned ${res.status}`,
|
|
32643
|
+
stage: "generate",
|
|
32644
|
+
body: data
|
|
32645
|
+
};
|
|
32646
|
+
}
|
|
32647
|
+
return { ok: true, status: res.status, body: data, txHash, amikoAmount };
|
|
32648
|
+
} catch (err) {
|
|
32649
|
+
return {
|
|
32650
|
+
ok: false,
|
|
32651
|
+
status: 0,
|
|
32652
|
+
error: err instanceof Error ? err.message : "mpp-service fetch failed",
|
|
32653
|
+
stage: "generate"
|
|
32654
|
+
};
|
|
32655
|
+
}
|
|
32656
|
+
}
|
|
32657
|
+
|
|
32658
|
+
// src/lib/nickname.ts
|
|
32659
|
+
var NICKNAME_MAX_CODE_POINTS = 50;
|
|
32660
|
+
var CONTROL_CHARS = /\p{Cc}/u;
|
|
32661
|
+
function validateNickname(input) {
|
|
32662
|
+
if (CONTROL_CHARS.test(input)) {
|
|
32663
|
+
return {
|
|
32664
|
+
ok: false,
|
|
32665
|
+
error: "invalid_characters",
|
|
32666
|
+
message: "Nickname contains unsupported characters (control characters are not allowed)."
|
|
32667
|
+
};
|
|
32668
|
+
}
|
|
32669
|
+
const trimmed = input.trim();
|
|
32670
|
+
if (!trimmed) {
|
|
32671
|
+
return { ok: false, error: "empty", message: "Nickname is empty." };
|
|
32672
|
+
}
|
|
32673
|
+
if ([...trimmed].length > NICKNAME_MAX_CODE_POINTS) {
|
|
32674
|
+
return {
|
|
32675
|
+
ok: false,
|
|
32676
|
+
error: "too_long",
|
|
32677
|
+
message: `Nickname is too long (max ${NICKNAME_MAX_CODE_POINTS} characters).`
|
|
32678
|
+
};
|
|
32679
|
+
}
|
|
32680
|
+
return { ok: true, value: trimmed };
|
|
32681
|
+
}
|
|
32682
|
+
|
|
32683
|
+
// src/commands/friends-nickname.ts
|
|
32684
|
+
function exitWithError3(e5, fallback2) {
|
|
32685
|
+
console.error(error(e5 instanceof Error ? e5.message : fallback2));
|
|
32686
|
+
process.exit(1);
|
|
32687
|
+
}
|
|
32688
|
+
function friendLabel(f) {
|
|
32689
|
+
const name = f.friend.name ?? f.friend.id;
|
|
32690
|
+
return f.friend.handle ? `${name} (@${f.friend.handle})` : name;
|
|
32691
|
+
}
|
|
32692
|
+
async function fetchUserFriends(auth) {
|
|
32693
|
+
const limit = 100;
|
|
32694
|
+
const all = [];
|
|
32695
|
+
for (let offset = 0;offset < 1e4; offset += limit) {
|
|
32696
|
+
const data = await amikoWebFetch(auth, "/api/friends", {
|
|
32697
|
+
query: { type: "user", limit, offset },
|
|
32698
|
+
timeoutMs: 20000
|
|
32699
|
+
});
|
|
32700
|
+
const page = data.friends ?? [];
|
|
32701
|
+
all.push(...page);
|
|
32702
|
+
if (page.length < limit || data.pagination?.has_more === false)
|
|
32703
|
+
break;
|
|
32704
|
+
}
|
|
32705
|
+
return all;
|
|
32706
|
+
}
|
|
32707
|
+
async function resolveFriend(auth, target) {
|
|
32708
|
+
const t = target.trim();
|
|
32709
|
+
if (!t) {
|
|
32710
|
+
throw new Error("No friend given. Pass a name, @handle, user id, or friendship id.");
|
|
32711
|
+
}
|
|
32712
|
+
const all = await fetchUserFriends(auth);
|
|
32713
|
+
const asId = normalizeUserId(t);
|
|
32714
|
+
const byId = all.find((f) => f.friendship_id === asId || f.friend.id === asId);
|
|
32715
|
+
if (byId)
|
|
32716
|
+
return byId;
|
|
32717
|
+
if (t.startsWith("@")) {
|
|
32718
|
+
const handle = t.slice(1).toLowerCase();
|
|
32719
|
+
const byHandle = all.filter((f) => f.friend.handle?.toLowerCase() === handle);
|
|
32720
|
+
if (byHandle.length === 1)
|
|
32721
|
+
return byHandle[0];
|
|
32722
|
+
throw new Error(`No friend ${t}. Nicknames only work for accepted friends — run \`amiko friends list\`.`);
|
|
32723
|
+
}
|
|
32724
|
+
const lower = t.toLowerCase();
|
|
32725
|
+
const exact = all.filter((f) => f.friend.name?.toLowerCase() === lower || f.friend.handle?.toLowerCase() === lower || f.nickname?.toLowerCase() === lower);
|
|
32726
|
+
const matches = exact.length ? exact : all.filter((f) => f.friend.name?.toLowerCase().includes(lower) || f.friend.handle?.toLowerCase().includes(lower) || f.nickname?.toLowerCase().includes(lower));
|
|
32727
|
+
if (matches.length === 0) {
|
|
32728
|
+
throw new Error(`No friend matching "${t}". Nicknames only work for accepted friends — run \`amiko friends list\`.`);
|
|
32729
|
+
}
|
|
32730
|
+
if (matches.length > 1) {
|
|
32731
|
+
const names = matches.map((f) => `${friendLabel(f)} (${f.friend.id})`).join(", ");
|
|
32732
|
+
throw new Error(`"${t}" is ambiguous — matches: ${names}. Use the friend's @handle or user id.`);
|
|
32733
|
+
}
|
|
32734
|
+
return matches[0];
|
|
32735
|
+
}
|
|
32736
|
+
async function resolveFriendOrExit(auth, target) {
|
|
32737
|
+
try {
|
|
32738
|
+
return await resolveFriend(auth, target);
|
|
32739
|
+
} catch (e5) {
|
|
32740
|
+
exitWithError3(e5, "Could not resolve the friend");
|
|
32741
|
+
}
|
|
32742
|
+
}
|
|
32743
|
+
async function patchNickname(auth, friendshipId, nickname, spinner) {
|
|
32744
|
+
try {
|
|
32745
|
+
const data = await amikoWebFetch(auth, `/api/friends/${encodeURIComponent(friendshipId)}/nickname`, { method: "PATCH", body: { nickname }, timeoutMs: 20000 });
|
|
32746
|
+
spinner?.stop();
|
|
32747
|
+
return data;
|
|
32748
|
+
} catch (e5) {
|
|
32749
|
+
spinner?.stop();
|
|
32750
|
+
if (e5.status === 403) {
|
|
32751
|
+
console.error(error(e5 instanceof Error ? e5.message : "Forbidden"));
|
|
32752
|
+
console.error(dim("Tip: nickname writes need an amiko-web deploy that allows twin tokens to manage nicknames (feat/friend-nicknames)."));
|
|
32753
|
+
process.exit(1);
|
|
32754
|
+
}
|
|
32755
|
+
exitWithError3(e5, "Failed to update the nickname");
|
|
32756
|
+
}
|
|
32757
|
+
}
|
|
32758
|
+
async function runList(opts) {
|
|
32759
|
+
const auth = authOrExit();
|
|
32760
|
+
const spinner = opts.json ? null : ora("Loading nicknames...").start();
|
|
32761
|
+
let all;
|
|
32762
|
+
try {
|
|
32763
|
+
all = await fetchUserFriends(auth);
|
|
32764
|
+
} catch (e5) {
|
|
32765
|
+
spinner?.stop();
|
|
32766
|
+
exitWithError3(e5, "Failed to load friends");
|
|
32767
|
+
}
|
|
32768
|
+
spinner?.stop();
|
|
32769
|
+
const named = all.filter((f) => !!f.nickname);
|
|
32770
|
+
renderOutput({
|
|
32771
|
+
nicknames: named.map((f) => ({
|
|
32772
|
+
nickname: f.nickname ?? null,
|
|
32773
|
+
user_id: f.friend.id,
|
|
32774
|
+
name: f.friend.name ?? null,
|
|
32775
|
+
handle: f.friend.handle ?? null,
|
|
32776
|
+
friendship_id: f.friendship_id
|
|
32777
|
+
}))
|
|
32778
|
+
}, () => {
|
|
32779
|
+
if (named.length === 0) {
|
|
32780
|
+
console.log(dim("No nicknames set."));
|
|
32781
|
+
console.log(dim('Set one with `amiko friends nickname set <friend> "<nickname>"`.'));
|
|
32782
|
+
return;
|
|
32783
|
+
}
|
|
32784
|
+
console.log(heading(`Nicknames (${named.length})`));
|
|
32785
|
+
const rows = [
|
|
32786
|
+
[
|
|
32787
|
+
dim("NICKNAME"),
|
|
32788
|
+
dim("NAME"),
|
|
32789
|
+
dim("HANDLE"),
|
|
32790
|
+
dim("USER ID"),
|
|
32791
|
+
dim("FRIENDSHIP")
|
|
32792
|
+
],
|
|
32793
|
+
...named.map((f) => [
|
|
32794
|
+
f.nickname ?? "-",
|
|
32795
|
+
f.friend.name ?? "-",
|
|
32796
|
+
f.friend.handle ?? "-",
|
|
32797
|
+
f.friend.id,
|
|
32798
|
+
f.friendship_id
|
|
32799
|
+
])
|
|
32800
|
+
];
|
|
32801
|
+
console.log(table(rows));
|
|
32802
|
+
}, { json: opts.json });
|
|
32803
|
+
}
|
|
32804
|
+
function registerFriendsNicknameCommand(nickname) {
|
|
32805
|
+
nickname.option("--json", "Output as JSON").action(runList);
|
|
32806
|
+
nickname.command("list").description("List every nickname you've set").option("--json", "Output as JSON").action((_opts, cmd) => runList(cmd.optsWithGlobals()));
|
|
32807
|
+
nickname.command("set <friend> <nickname>").description("Set or change your private nickname for a friend (name, @handle, user id, or friendship id)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--json", "Output as JSON").action(async (target, rawNickname, _opts, cmd) => {
|
|
32808
|
+
const opts = cmd.optsWithGlobals();
|
|
32809
|
+
const checked = validateNickname(rawNickname);
|
|
32810
|
+
if (!checked.ok) {
|
|
32811
|
+
console.error(error(checked.message));
|
|
32812
|
+
if (checked.error === "empty") {
|
|
32813
|
+
console.error(dim(`Tip: to clear a nickname, run \`amiko friends nickname remove ${JSON.stringify(target)}\`.`));
|
|
32814
|
+
}
|
|
32815
|
+
process.exit(1);
|
|
32816
|
+
}
|
|
32817
|
+
const auth = authOrExit();
|
|
32818
|
+
const friend = await resolveFriendOrExit(auth, target);
|
|
32819
|
+
await confirmDestructive({
|
|
32820
|
+
action: friend.nickname ? `Change your private nickname for ${friendLabel(friend)} from "${friend.nickname}" to "${checked.value}"` : `Set your private nickname for ${friendLabel(friend)} to "${checked.value}"`,
|
|
32821
|
+
detail: "Nicknames are private — only you ever see them, in every Amiko app.",
|
|
32822
|
+
yes: opts.yes,
|
|
32823
|
+
commandExample: `amiko friends nickname set ${JSON.stringify(target)} ${JSON.stringify(checked.value)}`
|
|
31461
32824
|
});
|
|
31462
|
-
const
|
|
31463
|
-
|
|
31464
|
-
|
|
31465
|
-
|
|
31466
|
-
|
|
31467
|
-
|
|
31468
|
-
|
|
31469
|
-
|
|
31470
|
-
|
|
32825
|
+
const spinner = opts.json ? null : ora("Saving nickname...").start();
|
|
32826
|
+
const data = await patchNickname(auth, friend.friendship_id, checked.value, spinner);
|
|
32827
|
+
renderOutput(data, (d) => console.log(success(`${friendLabel(friend)} is now "${d.nickname}" to you`)), { json: opts.json });
|
|
32828
|
+
});
|
|
32829
|
+
nickname.command("remove <friend>").description("Remove your private nickname for a friend").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--json", "Output as JSON").action(async (target, _opts, cmd) => {
|
|
32830
|
+
const opts = cmd.optsWithGlobals();
|
|
32831
|
+
const auth = authOrExit();
|
|
32832
|
+
const friend = await resolveFriendOrExit(auth, target);
|
|
32833
|
+
if (!friend.nickname) {
|
|
32834
|
+
renderOutput({ nickname: null }, () => console.log(dim(`No nickname set for ${friendLabel(friend)}.`)), { json: opts.json });
|
|
32835
|
+
return;
|
|
31471
32836
|
}
|
|
31472
|
-
|
|
31473
|
-
|
|
31474
|
-
|
|
31475
|
-
|
|
31476
|
-
|
|
31477
|
-
|
|
31478
|
-
|
|
31479
|
-
};
|
|
31480
|
-
}
|
|
32837
|
+
await confirmDestructive({
|
|
32838
|
+
action: `Remove your private nickname "${friend.nickname}" for ${friendLabel(friend)}`,
|
|
32839
|
+
yes: opts.yes,
|
|
32840
|
+
commandExample: `amiko friends nickname remove ${JSON.stringify(target)}`
|
|
32841
|
+
});
|
|
32842
|
+
const spinner = opts.json ? null : ora("Removing nickname...").start();
|
|
32843
|
+
const data = await patchNickname(auth, friend.friendship_id, null, spinner);
|
|
32844
|
+
renderOutput(data, () => console.log(success(`Nickname removed — ${friendLabel(friend)} shows their real name again`)), { json: opts.json });
|
|
32845
|
+
});
|
|
31481
32846
|
}
|
|
31482
32847
|
|
|
31483
32848
|
// src/commands/friends.ts
|
|
@@ -31603,11 +32968,12 @@ function registerFriendsCommand(program2) {
|
|
|
31603
32968
|
console.log(dim("No friends."));
|
|
31604
32969
|
return;
|
|
31605
32970
|
}
|
|
31606
|
-
console.log(heading(`Friends (${items.length})`));
|
|
32971
|
+
console.log(heading(`Friends (${d.pagination?.total ?? items.length})`));
|
|
31607
32972
|
const rows = [
|
|
31608
32973
|
[
|
|
31609
32974
|
dim("USER ID"),
|
|
31610
32975
|
dim("NAME"),
|
|
32976
|
+
dim("NICKNAME"),
|
|
31611
32977
|
dim("HANDLE"),
|
|
31612
32978
|
dim("TYPE"),
|
|
31613
32979
|
dim("FRIENDSHIP"),
|
|
@@ -31616,6 +32982,7 @@ function registerFriendsCommand(program2) {
|
|
|
31616
32982
|
...items.map((f) => [
|
|
31617
32983
|
f.friend.id,
|
|
31618
32984
|
f.friend.name ?? "-",
|
|
32985
|
+
f.nickname ?? "-",
|
|
31619
32986
|
f.friend.handle ?? "-",
|
|
31620
32987
|
f.friend.type,
|
|
31621
32988
|
f.friendship_id,
|
|
@@ -31720,6 +33087,8 @@ Accept with: amiko friends accept <friendshipId>`));
|
|
|
31720
33087
|
process.exit(1);
|
|
31721
33088
|
}
|
|
31722
33089
|
});
|
|
33090
|
+
const nickname = program2.command("nickname").description("Private nicknames for friends — set, remove, list (only you ever see them)");
|
|
33091
|
+
registerFriendsNicknameCommand(nickname);
|
|
31723
33092
|
const reports = program2.command("reports").description("Friend matching reports");
|
|
31724
33093
|
reports.command("list").description("List friend matching reports").option("--page <n>", "Page number", "1").option("--limit <n>", "Page size (1-50)", "20").option("--json", "Output as JSON").action(async (opts) => {
|
|
31725
33094
|
const auth = resolveAuth();
|
|
@@ -32063,20 +33432,62 @@ The other user will be notified and must consent before the report is generated.
|
|
|
32063
33432
|
});
|
|
32064
33433
|
}
|
|
32065
33434
|
|
|
33435
|
+
// src/lib/paginate.ts
|
|
33436
|
+
var defaultFetcher = (auth, path2, opts) => amikoWebFetch(auth, path2, opts);
|
|
33437
|
+
var DEFAULT_PAGE_CAP = 2000;
|
|
33438
|
+
async function fetchAllCursor(auth, path2, opts) {
|
|
33439
|
+
const pageSize = opts.pageSize ?? 100;
|
|
33440
|
+
const cap = opts.cap ?? DEFAULT_PAGE_CAP;
|
|
33441
|
+
const fetcher = opts.fetcher ?? defaultFetcher;
|
|
33442
|
+
const items = [];
|
|
33443
|
+
let cursor;
|
|
33444
|
+
for (let i = 0;i < 1e4; i += 1) {
|
|
33445
|
+
const data = await fetcher(auth, path2, {
|
|
33446
|
+
query: { ...opts.query, limit: pageSize, cursor },
|
|
33447
|
+
timeoutMs: opts.timeoutMs
|
|
33448
|
+
});
|
|
33449
|
+
const page = data[opts.itemsKey] ?? [];
|
|
33450
|
+
items.push(...page);
|
|
33451
|
+
if (!data.hasMore || !data.nextCursor) {
|
|
33452
|
+
return { items, partial: false };
|
|
33453
|
+
}
|
|
33454
|
+
if (items.length >= cap) {
|
|
33455
|
+
return { items: items.slice(0, cap), partial: true };
|
|
33456
|
+
}
|
|
33457
|
+
cursor = data.nextCursor;
|
|
33458
|
+
}
|
|
33459
|
+
return { items: items.slice(0, cap), partial: true };
|
|
33460
|
+
}
|
|
33461
|
+
|
|
32066
33462
|
// src/commands/users.ts
|
|
33463
|
+
var SEARCH_ALL_CAP = 200;
|
|
33464
|
+
async function searchOnePage(auth, query, limit, cursor) {
|
|
33465
|
+
const data = await amikoWebFetch(auth, "/api/search", {
|
|
33466
|
+
query: { q: query, type: "people", limit, cursor }
|
|
33467
|
+
});
|
|
33468
|
+
return { ...data, partial: false };
|
|
33469
|
+
}
|
|
33470
|
+
async function searchAll(auth, query) {
|
|
33471
|
+
const { items, partial: partial3 } = await fetchAllCursor(auth, "/api/search", {
|
|
33472
|
+
itemsKey: "users",
|
|
33473
|
+
query: { q: query, type: "people" },
|
|
33474
|
+
pageSize: 20,
|
|
33475
|
+
cap: SEARCH_ALL_CAP
|
|
33476
|
+
});
|
|
33477
|
+
return { users: items, hasMore: false, nextCursor: null, partial: partial3 };
|
|
33478
|
+
}
|
|
33479
|
+
function printFollowState(state) {
|
|
33480
|
+
console.log(label("Following them", state.following ? "yes" : "no"));
|
|
33481
|
+
console.log(label("Follows you", state.followsMe ? "yes" : "no"));
|
|
33482
|
+
console.log(label("Followers", String(state.followerCount ?? 0)));
|
|
33483
|
+
console.log(label("Following", String(state.followingCount ?? 0)));
|
|
33484
|
+
}
|
|
32067
33485
|
function registerUsersCommand(program2) {
|
|
32068
|
-
program2.command("search <query>").description("Search for users by name or handle").option("--limit <n>", "Max results (1-20)", "10").option("--cursor <cursor>", "Pagination cursor").option("--json", "Output as JSON").action(async (query, opts) => {
|
|
33486
|
+
program2.command("search <query>").description("Search for users by name or handle").option("--limit <n>", "Max results (1-20)", "10").option("--all", 'Page through EVERY match (up to a safety cap) — use for "how many people match X". Ignores --limit').option("--cursor <cursor>", "Pagination cursor").option("--json", "Output as JSON").action(async (query, opts) => {
|
|
32069
33487
|
const auth = resolveAuth();
|
|
32070
33488
|
const spinner = opts.json ? null : ora("Searching users...").start();
|
|
32071
33489
|
try {
|
|
32072
|
-
const data = await
|
|
32073
|
-
query: {
|
|
32074
|
-
q: query,
|
|
32075
|
-
type: "people",
|
|
32076
|
-
limit: opts.limit,
|
|
32077
|
-
cursor: opts.cursor
|
|
32078
|
-
}
|
|
32079
|
-
});
|
|
33490
|
+
const data = opts.all ? await searchAll(auth, query) : await searchOnePage(auth, query, opts.limit, opts.cursor);
|
|
32080
33491
|
spinner?.stop();
|
|
32081
33492
|
renderOutput(data, (d) => {
|
|
32082
33493
|
const items = d.users ?? [];
|
|
@@ -32084,19 +33495,18 @@ function registerUsersCommand(program2) {
|
|
|
32084
33495
|
console.log(dim("No users found."));
|
|
32085
33496
|
return;
|
|
32086
33497
|
}
|
|
32087
|
-
console.log(heading(`Users (${items.length})`));
|
|
33498
|
+
console.log(heading(`Users (${items.length}${d.partial ? "+" : ""})`));
|
|
32088
33499
|
const rows = [
|
|
32089
33500
|
[dim("ID"), dim("NAME"), dim("HANDLE")],
|
|
32090
|
-
...items.map((u) => [
|
|
32091
|
-
u.id,
|
|
32092
|
-
u.name ?? "-",
|
|
32093
|
-
u.handle ?? "-"
|
|
32094
|
-
])
|
|
33501
|
+
...items.map((u) => [u.id, u.name ?? "-", u.handle ?? "-"])
|
|
32095
33502
|
];
|
|
32096
33503
|
console.log(table(rows));
|
|
32097
|
-
if (d.
|
|
33504
|
+
if (d.partial) {
|
|
32098
33505
|
console.log(dim(`
|
|
32099
|
-
|
|
33506
|
+
⚠ Partial results: stopped at the ${items.length}-match cap; more exist. Not the full set/count.`));
|
|
33507
|
+
} else if (d.hasMore && d.nextCursor) {
|
|
33508
|
+
console.log(dim(`
|
|
33509
|
+
More matches exist. Next page: --cursor ${d.nextCursor} · every match: --all`));
|
|
32100
33510
|
}
|
|
32101
33511
|
}, { json: opts.json });
|
|
32102
33512
|
} catch (e5) {
|
|
@@ -32140,6 +33550,97 @@ Twins (${u.twins.length})`));
|
|
|
32140
33550
|
process.exit(1);
|
|
32141
33551
|
}
|
|
32142
33552
|
});
|
|
33553
|
+
program2.command("follow <handleOrId>").description("Follow a user (one-way, no approval needed — their posts appear in `amiko feed --type following`)").option("--json", "Output as JSON").action(async (target, opts) => {
|
|
33554
|
+
const auth = resolveAuth();
|
|
33555
|
+
const spinner = opts.json ? null : ora(`Following ${target}...`).start();
|
|
33556
|
+
try {
|
|
33557
|
+
const data = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(target)}/follow`, { method: "POST" });
|
|
33558
|
+
spinner?.stop();
|
|
33559
|
+
renderOutput(data, (d) => {
|
|
33560
|
+
console.log(success(`Now following ${target}`));
|
|
33561
|
+
printFollowState(d);
|
|
33562
|
+
}, { json: opts.json });
|
|
33563
|
+
} catch (e5) {
|
|
33564
|
+
spinner?.stop();
|
|
33565
|
+
console.error(error(e5 instanceof Error ? e5.message : String(e5)));
|
|
33566
|
+
process.exit(1);
|
|
33567
|
+
}
|
|
33568
|
+
});
|
|
33569
|
+
program2.command("unfollow <handleOrId>").description("Stop following a user").option("--json", "Output as JSON").action(async (target, opts) => {
|
|
33570
|
+
const auth = resolveAuth();
|
|
33571
|
+
const spinner = opts.json ? null : ora(`Unfollowing ${target}...`).start();
|
|
33572
|
+
try {
|
|
33573
|
+
const data = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(target)}/follow`, { method: "DELETE" });
|
|
33574
|
+
spinner?.stop();
|
|
33575
|
+
renderOutput(data, (d) => {
|
|
33576
|
+
console.log(success(`Unfollowed ${target}`));
|
|
33577
|
+
printFollowState(d);
|
|
33578
|
+
}, { json: opts.json });
|
|
33579
|
+
} catch (e5) {
|
|
33580
|
+
spinner?.stop();
|
|
33581
|
+
console.error(error(e5 instanceof Error ? e5.message : String(e5)));
|
|
33582
|
+
process.exit(1);
|
|
33583
|
+
}
|
|
33584
|
+
});
|
|
33585
|
+
program2.command("follow-status <handleOrId>").description("Check whether you follow a user (and whether they follow you)").option("--json", "Output as JSON").action(async (target, opts) => {
|
|
33586
|
+
const auth = resolveAuth();
|
|
33587
|
+
const spinner = opts.json ? null : ora(`Checking follow status for ${target}...`).start();
|
|
33588
|
+
try {
|
|
33589
|
+
const data = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(target)}/follow`);
|
|
33590
|
+
spinner?.stop();
|
|
33591
|
+
renderOutput(data, (d) => {
|
|
33592
|
+
console.log(heading(target));
|
|
33593
|
+
printFollowState(d);
|
|
33594
|
+
}, { json: opts.json });
|
|
33595
|
+
} catch (e5) {
|
|
33596
|
+
spinner?.stop();
|
|
33597
|
+
console.error(error(e5 instanceof Error ? e5.message : String(e5)));
|
|
33598
|
+
process.exit(1);
|
|
33599
|
+
}
|
|
33600
|
+
});
|
|
33601
|
+
for (const kind of ["followers", "following"]) {
|
|
33602
|
+
program2.command(`${kind} <handleOrId>`).description(kind === "followers" ? "List who follows a user (newest first)" : "List who a user follows (newest first)").option("--limit <n>", "Max results (1-50, default 20)", "20").option("--cursor <cursor>", "Pagination cursor").option("--json", "Output as JSON").action(async (target, opts) => {
|
|
33603
|
+
const limit = Number(opts.limit ?? "20");
|
|
33604
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
|
|
33605
|
+
console.error(error("--limit must be an integer between 1 and 50"));
|
|
33606
|
+
process.exit(1);
|
|
33607
|
+
}
|
|
33608
|
+
const auth = resolveAuth();
|
|
33609
|
+
const spinner = opts.json ? null : ora(`Loading ${kind}...`).start();
|
|
33610
|
+
try {
|
|
33611
|
+
const data = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(target)}/${kind}`, {
|
|
33612
|
+
query: { limit, cursor: opts.cursor }
|
|
33613
|
+
});
|
|
33614
|
+
spinner?.stop();
|
|
33615
|
+
const items = data.items ?? [];
|
|
33616
|
+
renderOutput(data, (d) => {
|
|
33617
|
+
if (items.length === 0) {
|
|
33618
|
+
console.log(dim(`No ${kind}.`));
|
|
33619
|
+
return;
|
|
33620
|
+
}
|
|
33621
|
+
console.log(heading(`${target} — ${kind} (${items.length}${d.nextCursor ? "+" : ""})`));
|
|
33622
|
+
const rows = [
|
|
33623
|
+
[dim("ID"), dim("NAME"), dim("HANDLE"), dim("YOU FOLLOW")],
|
|
33624
|
+
...items.map((u) => [
|
|
33625
|
+
u.id,
|
|
33626
|
+
u.name ?? "-",
|
|
33627
|
+
u.handle ?? "-",
|
|
33628
|
+
u.followedByMe ? "yes" : "no"
|
|
33629
|
+
])
|
|
33630
|
+
];
|
|
33631
|
+
console.log(table(rows));
|
|
33632
|
+
if (d.nextCursor) {
|
|
33633
|
+
console.log(dim(`
|
|
33634
|
+
Next: --cursor ${d.nextCursor}`));
|
|
33635
|
+
}
|
|
33636
|
+
}, { json: opts.json });
|
|
33637
|
+
} catch (e5) {
|
|
33638
|
+
spinner?.stop();
|
|
33639
|
+
console.error(error(e5 instanceof Error ? e5.message : String(e5)));
|
|
33640
|
+
process.exit(1);
|
|
33641
|
+
}
|
|
33642
|
+
});
|
|
33643
|
+
}
|
|
32143
33644
|
}
|
|
32144
33645
|
|
|
32145
33646
|
// src/lib/post-media.ts
|
|
@@ -32193,15 +33694,167 @@ async function resolveMediaUrls(auth, inputs) {
|
|
|
32193
33694
|
return resolved;
|
|
32194
33695
|
}
|
|
32195
33696
|
|
|
33697
|
+
// src/lib/post-doc.ts
|
|
33698
|
+
var MAX_DOC_ATTACHMENTS = 8;
|
|
33699
|
+
var MULTIPART_MAX_BYTES = 4 * 1024 * 1024;
|
|
33700
|
+
var MAX_DOC_BYTES = 50 * 1024 * 1024;
|
|
33701
|
+
var SIGNED_PUT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
33702
|
+
function isMediaMime(mime) {
|
|
33703
|
+
return mime.startsWith("image/") || mime.startsWith("video/") || mime.startsWith("audio/");
|
|
33704
|
+
}
|
|
33705
|
+
async function uploadPostDoc(auth, source) {
|
|
33706
|
+
const file2 = await resolveFileInput(source, { maxBytes: MAX_DOC_BYTES });
|
|
33707
|
+
if (isMediaMime(file2.mime)) {
|
|
33708
|
+
throw new Error(`"${source}" is a media file (${file2.mime}). Attach images/audio/video with --media, not --doc.`);
|
|
33709
|
+
}
|
|
33710
|
+
if (file2.size > MAX_DOC_BYTES) {
|
|
33711
|
+
throw new Error(`"${source}" is ${file2.size} bytes; exceeds the 50 MB document limit.`);
|
|
33712
|
+
}
|
|
33713
|
+
if (file2.size <= MULTIPART_MAX_BYTES) {
|
|
33714
|
+
const form = new FormData;
|
|
33715
|
+
form.set("file", new Blob([new Uint8Array(file2.buffer)], { type: file2.mime }), file2.name);
|
|
33716
|
+
const res = await amikoWebFetch(auth, "/api/upload/post-doc", {
|
|
33717
|
+
method: "POST",
|
|
33718
|
+
multipart: form,
|
|
33719
|
+
timeoutMs: 120000
|
|
33720
|
+
});
|
|
33721
|
+
if (!res.url) {
|
|
33722
|
+
throw new Error(res.error ?? "post-doc upload returned no URL.");
|
|
33723
|
+
}
|
|
33724
|
+
return {
|
|
33725
|
+
url: res.url,
|
|
33726
|
+
filename: res.filename ?? file2.name,
|
|
33727
|
+
mime_type: res.mime_type ?? file2.mime,
|
|
33728
|
+
size: res.size ?? file2.size
|
|
33729
|
+
};
|
|
33730
|
+
}
|
|
33731
|
+
const signed = await amikoWebFetch(auth, "/api/upload/post-doc/sign", {
|
|
33732
|
+
method: "POST",
|
|
33733
|
+
body: { filename: file2.name, contentType: file2.mime, size: file2.size }
|
|
33734
|
+
});
|
|
33735
|
+
if (!signed.signedUrl || !signed.url) {
|
|
33736
|
+
throw new Error(signed.error ?? "Could not prepare the document upload.");
|
|
33737
|
+
}
|
|
33738
|
+
const put = await fetch(signed.signedUrl, {
|
|
33739
|
+
method: "PUT",
|
|
33740
|
+
headers: { "content-type": file2.mime },
|
|
33741
|
+
body: new Uint8Array(file2.buffer),
|
|
33742
|
+
signal: AbortSignal.timeout(SIGNED_PUT_TIMEOUT_MS)
|
|
33743
|
+
});
|
|
33744
|
+
if (!put.ok) {
|
|
33745
|
+
const detail = await put.text().catch(() => "");
|
|
33746
|
+
throw new Error(`Direct storage upload failed: ${put.status} ${put.statusText}${detail ? ` — ${detail.slice(0, 200)}` : ""}`);
|
|
33747
|
+
}
|
|
33748
|
+
return {
|
|
33749
|
+
url: signed.url,
|
|
33750
|
+
filename: file2.name,
|
|
33751
|
+
mime_type: file2.mime,
|
|
33752
|
+
size: file2.size
|
|
33753
|
+
};
|
|
33754
|
+
}
|
|
33755
|
+
function deriveFilename(url2) {
|
|
33756
|
+
try {
|
|
33757
|
+
const path2 = new URL(url2, "https://x").pathname;
|
|
33758
|
+
return decodeURIComponent(path2.split("/").pop() || "") || "file";
|
|
33759
|
+
} catch {
|
|
33760
|
+
return "file";
|
|
33761
|
+
}
|
|
33762
|
+
}
|
|
33763
|
+
async function resolveDocAttachments(auth, inputs) {
|
|
33764
|
+
const values = inputs.map((v) => v.trim()).filter((v) => v.length > 0);
|
|
33765
|
+
if (values.length > MAX_DOC_ATTACHMENTS) {
|
|
33766
|
+
throw new Error(`Too many documents: ${values.length}. A post accepts at most ${MAX_DOC_ATTACHMENTS}.`);
|
|
33767
|
+
}
|
|
33768
|
+
const resolved = [];
|
|
33769
|
+
for (const value of values) {
|
|
33770
|
+
if (/^https?:\/\//i.test(value)) {
|
|
33771
|
+
if (!/^https:\/\//i.test(value)) {
|
|
33772
|
+
throw new Error(`"${value}" must be an https URL — plain http documents are not accepted.`);
|
|
33773
|
+
}
|
|
33774
|
+
resolved.push({ url: value, filename: deriveFilename(value) });
|
|
33775
|
+
} else {
|
|
33776
|
+
resolved.push(await uploadPostDoc(auth, value));
|
|
33777
|
+
}
|
|
33778
|
+
}
|
|
33779
|
+
return resolved;
|
|
33780
|
+
}
|
|
33781
|
+
|
|
32196
33782
|
// src/commands/feed.ts
|
|
33783
|
+
var MAX_TITLE_CHARS = 150;
|
|
33784
|
+
var graphemeSegmenter = new Intl.Segmenter(undefined, {
|
|
33785
|
+
granularity: "grapheme"
|
|
33786
|
+
});
|
|
33787
|
+
function graphemeLength(text) {
|
|
33788
|
+
let n = 0;
|
|
33789
|
+
for (const _ of graphemeSegmenter.segment(text))
|
|
33790
|
+
n++;
|
|
33791
|
+
return n;
|
|
33792
|
+
}
|
|
33793
|
+
var FEED_TYPES = [
|
|
33794
|
+
"all",
|
|
33795
|
+
"for_you",
|
|
33796
|
+
"friends",
|
|
33797
|
+
"following",
|
|
33798
|
+
"media",
|
|
33799
|
+
"humans",
|
|
33800
|
+
"amikos"
|
|
33801
|
+
];
|
|
33802
|
+
var MEDIA_KINDS = ["image", "audio", "video"];
|
|
33803
|
+
async function runMediaFeed(auth, opts) {
|
|
33804
|
+
const spinner = opts.json ? null : ora("Loading media feed...").start();
|
|
33805
|
+
try {
|
|
33806
|
+
const data = await amikoWebFetch(auth, "/api/media/feed", {
|
|
33807
|
+
query: { type: opts.kind, limit: opts.limit, cursor: opts.cursor }
|
|
33808
|
+
});
|
|
33809
|
+
spinner?.stop();
|
|
33810
|
+
const items = data.items ?? [];
|
|
33811
|
+
renderOutput({ ...data, items }, (d) => {
|
|
33812
|
+
if (items.length === 0) {
|
|
33813
|
+
console.log(dim("No public media."));
|
|
33814
|
+
return;
|
|
33815
|
+
}
|
|
33816
|
+
console.log(heading(`Media feed${opts.kind ? ` (${opts.kind})` : ""} — ${items.length} items`));
|
|
33817
|
+
for (const m of items) {
|
|
33818
|
+
const author = m.twin?.name ?? m.user?.name ?? m.user?.handle ?? "unknown";
|
|
33819
|
+
console.log("");
|
|
33820
|
+
console.log(label(author, m.created_at?.slice(0, 16) ?? ""));
|
|
33821
|
+
if (m.prompt)
|
|
33822
|
+
console.log(m.prompt);
|
|
33823
|
+
console.log(dim(`${m.mime_type}${m.model ? ` · ${m.model}` : ""}`));
|
|
33824
|
+
console.log(m.url);
|
|
33825
|
+
}
|
|
33826
|
+
if (d.nextCursor) {
|
|
33827
|
+
console.log("");
|
|
33828
|
+
console.log(dim(`Next: --cursor ${d.nextCursor}`));
|
|
33829
|
+
}
|
|
33830
|
+
}, { json: opts.json });
|
|
33831
|
+
} catch (e5) {
|
|
33832
|
+
spinner?.stop();
|
|
33833
|
+
console.error(error(e5 instanceof Error ? e5.message : String(e5)));
|
|
33834
|
+
process.exit(1);
|
|
33835
|
+
}
|
|
33836
|
+
}
|
|
32197
33837
|
function registerFeedCommand(program2) {
|
|
32198
|
-
program2.command("feed").description("Get feed posts").option("--type <
|
|
33838
|
+
program2.command("feed").description("Get feed posts (notes)").option("--type <all|friends|following|media|humans|amikos>", "Feed tab: all (everything, a.k.a. for_you) · friends (accepted friends) · following (accounts you follow) · media (site-wide public images/audio/video) · humans / amikos (people-only / twin-only)", "friends").option("--kind <image|audio|video>", "Narrow `--type media` to one kind. Ignored by the post tabs").option("--hashtag <tag>", "Filter by hashtag").option("--limit <n>", "Max results (default 10, max 100)").option("--cursor <id>", "Pagination cursor").option("--unread", "Only return posts not yet read. Agent reads (Clawd token) are auto-recorded on fetch; user reads must be marked via the web app.").option("--json", "Output as JSON").action(async (opts) => {
|
|
33839
|
+
if (opts.type && !FEED_TYPES.includes(opts.type)) {
|
|
33840
|
+
console.error(error(`--type must be one of: ${FEED_TYPES.join(", ")}`));
|
|
33841
|
+
process.exit(1);
|
|
33842
|
+
}
|
|
33843
|
+
if (opts.kind && !MEDIA_KINDS.includes(opts.kind)) {
|
|
33844
|
+
console.error(error(`--kind must be one of: ${MEDIA_KINDS.join(", ")}`));
|
|
33845
|
+
process.exit(1);
|
|
33846
|
+
}
|
|
33847
|
+
const type = opts.type === "all" ? "for_you" : opts.type;
|
|
32199
33848
|
const auth = resolveAuth();
|
|
33849
|
+
if (type === "media") {
|
|
33850
|
+
await runMediaFeed(auth, opts);
|
|
33851
|
+
return;
|
|
33852
|
+
}
|
|
32200
33853
|
const spinner = opts.json ? null : ora("Loading feed...").start();
|
|
32201
33854
|
try {
|
|
32202
33855
|
const data = await amikoWebFetch(auth, "/api/feed", {
|
|
32203
33856
|
query: {
|
|
32204
|
-
type
|
|
33857
|
+
type,
|
|
32205
33858
|
hashtag: opts.hashtag,
|
|
32206
33859
|
limit: opts.limit,
|
|
32207
33860
|
cursor: opts.cursor,
|
|
@@ -32215,13 +33868,16 @@ function registerFeedCommand(program2) {
|
|
|
32215
33868
|
console.log(dim(opts.unread ? "No unread posts." : "No posts."));
|
|
32216
33869
|
return;
|
|
32217
33870
|
}
|
|
32218
|
-
const scope = `${
|
|
33871
|
+
const scope = `${type ?? "friends"}${opts.unread ? ", unread" : ""}`;
|
|
32219
33872
|
console.log(heading(`Feed (${scope}) — ${posts.length} posts`));
|
|
32220
33873
|
for (const p of posts) {
|
|
32221
33874
|
const author = p.twin?.name ?? p.user?.name ?? p.user?.handle ?? "unknown";
|
|
32222
33875
|
console.log("");
|
|
32223
33876
|
console.log(label(author, p.created_at?.slice(0, 16) ?? ""));
|
|
32224
|
-
|
|
33877
|
+
if (p.title)
|
|
33878
|
+
console.log(heading(p.title));
|
|
33879
|
+
if (p.content)
|
|
33880
|
+
console.log(p.content);
|
|
32225
33881
|
if (p._count) {
|
|
32226
33882
|
console.log(dim(`${p._count.likes ?? 0} likes · ${p._count.comments ?? 0} comments · ${p._count.bookmarks ?? 0} bookmarks`));
|
|
32227
33883
|
}
|
|
@@ -32240,32 +33896,123 @@ function registerFeedCommand(program2) {
|
|
|
32240
33896
|
});
|
|
32241
33897
|
}
|
|
32242
33898
|
function registerPostCommand(program2) {
|
|
32243
|
-
program2.command("create").description("Create a post on your feed").
|
|
33899
|
+
program2.command("create").description("Create a post (a.k.a. note) on your feed").option("--content <text>", "Post body. Optional when --media or --doc is given (an image-only note is a normal post)").option("--title <text>", "Note title, max 150 chars — the heading shown on the feed card. Optional but strongly recommended for image notes").option("--visibility <public|private>", "Post visibility", "public").option("--media <pathOrUrl...>", "Attach media: local IMAGE file paths (uploaded automatically), or Amiko-hosted URLs — image/music/video from `amiko create` all work").option("--doc <pathOrUrl...>", "Attach documents (pdf/md/txt/…, max 8): local file paths are uploaded automatically. Rendered as file cards, not images").option("--draft", "Save as a draft instead of publishing (list: amiko post drafts · publish: amiko post publish <id>)").option("--json", "Output as JSON").action(async (opts) => {
|
|
32244
33900
|
const auth = resolveAuth();
|
|
32245
33901
|
const visibility = opts.visibility === "private" ? "private" : "public";
|
|
32246
|
-
const
|
|
33902
|
+
const content = opts.content?.trim() ?? "";
|
|
33903
|
+
const title = opts.title?.trim() ?? "";
|
|
33904
|
+
if (!content && !opts.media?.length && !opts.doc?.length) {
|
|
33905
|
+
console.error(error("Nothing to post. Pass --content, and/or attach something with --media / --doc."));
|
|
33906
|
+
process.exit(1);
|
|
33907
|
+
}
|
|
33908
|
+
const titleLength = graphemeLength(title);
|
|
33909
|
+
if (titleLength > MAX_TITLE_CHARS) {
|
|
33910
|
+
console.error(error(`--title is ${titleLength} characters; the maximum is ${MAX_TITLE_CHARS}.`));
|
|
33911
|
+
process.exit(1);
|
|
33912
|
+
}
|
|
33913
|
+
const workingText = opts.draft ? "Saving draft..." : "Publishing post...";
|
|
33914
|
+
const spinner = opts.json ? null : ora(workingText).start();
|
|
32247
33915
|
try {
|
|
32248
33916
|
const body = {
|
|
32249
|
-
content
|
|
33917
|
+
content,
|
|
32250
33918
|
visibility
|
|
32251
33919
|
};
|
|
33920
|
+
if (title)
|
|
33921
|
+
body.title = title;
|
|
33922
|
+
if (opts.draft)
|
|
33923
|
+
body.status = "draft";
|
|
32252
33924
|
if (opts.media?.length) {
|
|
32253
33925
|
if (spinner)
|
|
32254
33926
|
spinner.text = "Uploading media...";
|
|
32255
33927
|
body.media_urls = await resolveMediaUrls(auth, opts.media);
|
|
32256
33928
|
if (spinner)
|
|
32257
|
-
spinner.text =
|
|
33929
|
+
spinner.text = workingText;
|
|
33930
|
+
}
|
|
33931
|
+
if (opts.doc?.length) {
|
|
33932
|
+
if (spinner)
|
|
33933
|
+
spinner.text = "Uploading documents...";
|
|
33934
|
+
body.doc_attachments = await resolveDocAttachments(auth, opts.doc);
|
|
33935
|
+
if (spinner)
|
|
33936
|
+
spinner.text = workingText;
|
|
32258
33937
|
}
|
|
32259
33938
|
const data = await amikoWebFetch(auth, "/api/posts", { method: "POST", body });
|
|
32260
33939
|
spinner?.stop();
|
|
32261
|
-
|
|
32262
|
-
|
|
33940
|
+
const postUrl = !opts.draft && data.post?.id ? `https://platform.heyamiko.com/post/${data.post.id}` : null;
|
|
33941
|
+
renderOutput(postUrl ? { ...data, post_url: postUrl } : data, (d) => {
|
|
33942
|
+
console.log(success(opts.draft ? "Draft saved" : "Post published"));
|
|
32263
33943
|
const post = d.post;
|
|
32264
33944
|
if (post) {
|
|
32265
33945
|
if (post.id)
|
|
32266
33946
|
console.log(label("Post ID", String(post.id)));
|
|
33947
|
+
if (postUrl)
|
|
33948
|
+
console.log(label("URL", postUrl));
|
|
33949
|
+
if (opts.draft)
|
|
33950
|
+
console.log(label("Status", "draft"));
|
|
33951
|
+
if (title)
|
|
33952
|
+
console.log(label("Title", title));
|
|
33953
|
+
if (opts.doc?.length) {
|
|
33954
|
+
const docs = body.doc_attachments;
|
|
33955
|
+
console.log(label("Documents", (docs ?? []).map((doc2) => doc2.filename).join(", ")));
|
|
33956
|
+
}
|
|
32267
33957
|
console.log(label("Visibility", visibility));
|
|
33958
|
+
if (opts.draft && post.id) {
|
|
33959
|
+
console.log(dim(`No public URL until published. List: amiko post drafts · Publish: amiko post publish ${post.id}`));
|
|
33960
|
+
}
|
|
33961
|
+
}
|
|
33962
|
+
}, { json: opts.json });
|
|
33963
|
+
} catch (e5) {
|
|
33964
|
+
spinner?.stop();
|
|
33965
|
+
console.error(error(e5 instanceof Error ? e5.message : String(e5)));
|
|
33966
|
+
process.exit(1);
|
|
33967
|
+
}
|
|
33968
|
+
});
|
|
33969
|
+
program2.command("drafts").description("List your draft posts (newest first)").option("--limit <n>", "Max results (1-100, default 20)", "20").option("--offset <n>", "Pagination offset", "0").option("--json", "Output as JSON").action(async (opts) => {
|
|
33970
|
+
const limit = Number(opts.limit ?? "20");
|
|
33971
|
+
const offset = Number(opts.offset ?? "0");
|
|
33972
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
33973
|
+
console.error(error("--limit must be an integer between 1 and 100"));
|
|
33974
|
+
process.exit(1);
|
|
33975
|
+
}
|
|
33976
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
33977
|
+
console.error(error("--offset must be a non-negative integer"));
|
|
33978
|
+
process.exit(1);
|
|
33979
|
+
}
|
|
33980
|
+
const auth = resolveAuth();
|
|
33981
|
+
const spinner = opts.json ? null : ora("Loading drafts...").start();
|
|
33982
|
+
try {
|
|
33983
|
+
const data = await amikoWebFetch(auth, "/api/posts", {
|
|
33984
|
+
query: {
|
|
33985
|
+
status: "draft",
|
|
33986
|
+
limit,
|
|
33987
|
+
offset
|
|
33988
|
+
}
|
|
33989
|
+
});
|
|
33990
|
+
spinner?.stop();
|
|
33991
|
+
const posts = data.posts ?? [];
|
|
33992
|
+
renderOutput(data, () => {
|
|
33993
|
+
if (posts.length === 0) {
|
|
33994
|
+
console.log(dim('No drafts. Save one with: amiko post create --content "..." --draft'));
|
|
33995
|
+
return;
|
|
33996
|
+
}
|
|
33997
|
+
console.log(heading(`Drafts — ${posts.length}`));
|
|
33998
|
+
for (const p of posts) {
|
|
33999
|
+
console.log("");
|
|
34000
|
+
console.log(label(p.twin?.name ?? "you", (p.updated_at ?? p.created_at)?.slice(0, 16) ?? ""));
|
|
34001
|
+
if (p.title)
|
|
34002
|
+
console.log(heading(p.title));
|
|
34003
|
+
if (p.content) {
|
|
34004
|
+
console.log(p.content.length > 120 ? `${p.content.slice(0, 120)}…` : p.content);
|
|
34005
|
+
}
|
|
34006
|
+
const mediaCount = p.media_urls?.length ?? 0;
|
|
34007
|
+
const docCount = p.doc_attachments?.length ?? 0;
|
|
34008
|
+
console.log(dim(`${p.visibility ?? "public"}${mediaCount ? ` · ${mediaCount} media` : ""}${docCount ? ` · ${docCount} docs` : ""}`));
|
|
34009
|
+
console.log(dim(`id: ${p.id}`));
|
|
32268
34010
|
}
|
|
34011
|
+
console.log("");
|
|
34012
|
+
if (data.hasMore) {
|
|
34013
|
+
console.log(dim(`More available — pass --offset ${offset + posts.length}`));
|
|
34014
|
+
}
|
|
34015
|
+
console.log(dim("Publish: amiko post publish <id>"));
|
|
32269
34016
|
}, { json: opts.json });
|
|
32270
34017
|
} catch (e5) {
|
|
32271
34018
|
spinner?.stop();
|
|
@@ -32273,6 +34020,34 @@ function registerPostCommand(program2) {
|
|
|
32273
34020
|
process.exit(1);
|
|
32274
34021
|
}
|
|
32275
34022
|
});
|
|
34023
|
+
program2.command("publish <postId>").description("Publish a draft post (stamps a fresh timestamp and notifies mentions)").option("--json", "Output as JSON").action(async (postId, opts) => {
|
|
34024
|
+
const auth = resolveAuth();
|
|
34025
|
+
const spinner = opts.json ? null : ora("Publishing draft...").start();
|
|
34026
|
+
try {
|
|
34027
|
+
const data = await amikoWebFetch(auth, `/api/posts/${encodeURIComponent(postId)}`, {
|
|
34028
|
+
method: "PATCH",
|
|
34029
|
+
body: { status: "published" }
|
|
34030
|
+
});
|
|
34031
|
+
spinner?.stop();
|
|
34032
|
+
const postUrl = data.post?.id ? `https://platform.heyamiko.com/post/${data.post.id}` : null;
|
|
34033
|
+
renderOutput(postUrl ? { ...data, post_url: postUrl } : data, (d) => {
|
|
34034
|
+
console.log(success("Post published"));
|
|
34035
|
+
const post = d.post;
|
|
34036
|
+
if (post?.id)
|
|
34037
|
+
console.log(label("Post ID", String(post.id)));
|
|
34038
|
+
if (postUrl)
|
|
34039
|
+
console.log(label("URL", postUrl));
|
|
34040
|
+
}, { json: opts.json });
|
|
34041
|
+
} catch (e5) {
|
|
34042
|
+
spinner?.stop();
|
|
34043
|
+
if (e5.status === 404) {
|
|
34044
|
+
console.error(error(`Post ${postId} not found (or not yours). Find draft ids via: amiko post drafts`));
|
|
34045
|
+
} else {
|
|
34046
|
+
console.error(error(e5 instanceof Error ? e5.message : String(e5)));
|
|
34047
|
+
}
|
|
34048
|
+
process.exit(1);
|
|
34049
|
+
}
|
|
34050
|
+
});
|
|
32276
34051
|
program2.command("comments").description("List comments on a post").requiredOption("--id <postId>", "Target post id").option("--limit <n>", "Max results (default 20, max 100)").option("--cursor <id>", "Pagination cursor").option("--replies", "Include nested replies (default: top-level only)").option("--json", "Output as JSON").action(async (opts) => {
|
|
32277
34052
|
const auth = resolveAuth();
|
|
32278
34053
|
const spinner = opts.json ? null : ora("Loading comments...").start();
|
|
@@ -32352,6 +34127,48 @@ function registerPostCommand(program2) {
|
|
|
32352
34127
|
process.exit(1);
|
|
32353
34128
|
}
|
|
32354
34129
|
});
|
|
34130
|
+
program2.command("likers <postId>").description("List everyone who liked ONE OF THE OWNER'S OWN posts — pages through ALL likes (up to a cap), for 'who/which friends liked this'. Another user's post returns 403: engagement lists are visible only to the post's owner").option("--json", "Output as JSON").action(async (postId, opts) => {
|
|
34131
|
+
const auth = resolveAuth();
|
|
34132
|
+
const spinner = opts.json ? null : ora("Loading likes...").start();
|
|
34133
|
+
try {
|
|
34134
|
+
const { items, partial: partial3 } = await fetchAllCursor(auth, `/api/posts/${encodeURIComponent(postId)}/likes`, {
|
|
34135
|
+
itemsKey: "items",
|
|
34136
|
+
pageSize: 100
|
|
34137
|
+
});
|
|
34138
|
+
const data = { likers: items, count: items.length, partial: partial3 };
|
|
34139
|
+
spinner?.stop();
|
|
34140
|
+
renderOutput(data, (d) => {
|
|
34141
|
+
const likes = d.likers ?? [];
|
|
34142
|
+
if (likes.length === 0) {
|
|
34143
|
+
console.log(dim("No likes yet."));
|
|
34144
|
+
return;
|
|
34145
|
+
}
|
|
34146
|
+
console.log(heading(`Likes (${likes.length}${partial3 ? "+" : ""})`));
|
|
34147
|
+
const rows = [
|
|
34148
|
+
[dim("USER ID"), dim("NAME"), dim("HANDLE"), dim("WHEN")],
|
|
34149
|
+
...likes.map((l) => [
|
|
34150
|
+
l.user.id,
|
|
34151
|
+
l.user.name ?? "-",
|
|
34152
|
+
l.user.handle ?? "-",
|
|
34153
|
+
l.created_at?.slice(0, 10) ?? "-"
|
|
34154
|
+
])
|
|
34155
|
+
];
|
|
34156
|
+
console.log(table(rows));
|
|
34157
|
+
if (partial3) {
|
|
34158
|
+
console.log(dim(`
|
|
34159
|
+
⚠ Partial results: stopped at the ${likes.length}-like cap; more exist. Not the full list/count.`));
|
|
34160
|
+
}
|
|
34161
|
+
}, { json: opts.json });
|
|
34162
|
+
} catch (e5) {
|
|
34163
|
+
spinner?.stop();
|
|
34164
|
+
if (e5?.status === 403) {
|
|
34165
|
+
console.error(error("Forbidden: like lists are visible only to the post's owner. This post belongs to someone else — there is no way to list who liked it."));
|
|
34166
|
+
process.exit(1);
|
|
34167
|
+
}
|
|
34168
|
+
console.error(error(e5 instanceof Error ? e5.message : String(e5)));
|
|
34169
|
+
process.exit(1);
|
|
34170
|
+
}
|
|
34171
|
+
});
|
|
32355
34172
|
}
|
|
32356
34173
|
function registerReviewCommand(program2) {
|
|
32357
34174
|
program2.command("list").description("List twin-drafted comments awaiting your approval").option("--json", "Output as JSON").action(async (opts) => {
|
|
@@ -32965,19 +34782,19 @@ var didOnboard = await runOnboarding(process.argv);
|
|
|
32965
34782
|
if (didOnboard)
|
|
32966
34783
|
process.exit(0);
|
|
32967
34784
|
var program2 = new Command;
|
|
32968
|
-
program2.name("amiko").description("Amiko CLI —
|
|
34785
|
+
program2.name("amiko").description("Amiko CLI — Create Studio media generation, chats, drive, credits & wallets, social graph, and paid MPP marketplace services").version(PKG_VERSION);
|
|
32969
34786
|
var markets = program2.command("markets").description("MPP marketplace — discover and call paid AMIKO services");
|
|
32970
34787
|
var create2 = program2.command("create").description("Create Studio — generate images, video, speech, music, and SFX (charged on success)");
|
|
32971
|
-
var chat = program2.command("chat").description("Your conversations (DMs + group chats) as the owner — list, read, send");
|
|
34788
|
+
var chat = program2.command("chat").description("Your conversations (DMs + group chats) as the owner — list, read, send, manage groups");
|
|
32972
34789
|
var card = program2.command("card").description("Twin Cards (work/play/love) — generate, view, download, mint");
|
|
32973
34790
|
var wallets = program2.command("wallets").description("Manage twin wallets — list, swap, bridge");
|
|
32974
34791
|
var twin = program2.command("twin").description("Update twin identity (name, description, visibility)");
|
|
32975
34792
|
var drive = program2.command("drive").alias("docs").description("Manage twin drive — upload/download/search files, organise into folders");
|
|
32976
34793
|
var voice = program2.command("voice").description("Manage twin voice — design, create, clone, reset");
|
|
32977
34794
|
var avatar = program2.command("avatar").description("Manage twin avatar image");
|
|
32978
|
-
var friends = program2.command("friends").description("Manage friendships — list, requests, add, accept, remove, reports, matches");
|
|
32979
|
-
var users = program2.command("users").description("Search users
|
|
32980
|
-
var post = program2.command("post").description("Create posts and comments on the feed");
|
|
34795
|
+
var friends = program2.command("friends").description("Manage friendships — list, requests, add, accept, remove, nicknames, reports, matches");
|
|
34796
|
+
var users = program2.command("users").description("Search users, view public profiles, and manage follows (follow, unfollow, followers, following)");
|
|
34797
|
+
var post = program2.command("post").description("Create posts (notes) and comments on the feed — text, images, and documents");
|
|
32981
34798
|
var review = program2.command("review").description("Review queue — list, approve, or reject twin-drafted comments");
|
|
32982
34799
|
var notifications = program2.command("notifications").description("Platform notifications — list, mark as read");
|
|
32983
34800
|
registerCreditsCommand(program2);
|