@heyamiko/amiko-cli 0.14.1 → 0.14.2
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 +9 -0
- package/dist/index.js +57 -6
- package/package.json +1 -1
- package/skills/SKILL.md +9 -2
package/README.md
CHANGED
|
@@ -607,6 +607,7 @@ amiko post create --title "Kyoto, 6am" --media ./shot.webp # image-only note —
|
|
|
607
607
|
amiko post create --title "Q3 report" --doc ./q3.pdf # document note — rendered as a file card
|
|
608
608
|
amiko post create --content "private note" --visibility private
|
|
609
609
|
amiko post create --content "look" --media https://...jpg
|
|
610
|
+
amiko post create --title "the track" --media drive:<docId> # media already in the Drive (id from `amiko drive list`) — works for audio/video too
|
|
610
611
|
amiko post create --content "wip idea" --draft # save a draft — no share URL until published
|
|
611
612
|
amiko post drafts # list draft posts (--limit/--offset/--json)
|
|
612
613
|
amiko post likers <postId> # who liked one of YOUR posts (403 on someone else's)
|
|
@@ -635,6 +636,14 @@ npm publish
|
|
|
635
636
|
|
|
636
637
|
## Changelog
|
|
637
638
|
|
|
639
|
+
### 0.14.2
|
|
640
|
+
|
|
641
|
+
- **`--media drive:<docId>`** (new source for `post create` and `post comment`): attach a media file already in the twin's Drive — an upload, or a Create Studio generation, which `saveCreationToDrive` mirrors into the "Create Studio Files" folder. The id comes from `amiko drive list --json`. This closes the gap where the only way to post an *audio or video* file the twin already owned was to have kept the original `amiko create` URL: `--media` takes local paths for images only, and a Drive signed URL expires.
|
|
642
|
+
- **No amiko-web change was needed, and this is why.** A Drive doc's `file_url` is stored in *public* form (`…/storage/v1/object/public/docs/<path>`) by all three write paths (`uploadDocumentToSupabase`, `createDocSignedUpload`, `saveCreationToDrive`) even though the `docs` bucket is private. `/api/posts` accepts it (`isAllowedPostMediaUrl` checks host + `/storage/v1/object/` only), then `ensurePostMediaUrl` sees a non-`post-media` bucket and does a **service-role server-side copy into `post-media`**, returning a durable URL — so the post never links the private object and never expires. If that copy fails, the fallback reachability probe on a private-bucket URL 4xxs and the route rejects the post rather than storing a dead link. This mirrors what amiko-web's own `DriveMediaPicker` already does (it forwards the same `file_url` under the same MIME allowlist), so the CLI is matching shipped behaviour, not inventing a path.
|
|
643
|
+
- **The drive ref honours the command's `--twin`.** Docs are scoped by `twin_id`; `resolveMediaUrls` now takes the caller's `--twin` instead of re-resolving the default twin, so `post comment --twin <other> --media drive:<id>` no longer 404s on an id `drive list --twin <other>` had just printed.
|
|
644
|
+
- **Two guards on the resolved row.** A `file_url` that isn't Amiko public storage is refused locally — only `source`-tagged Doc rows are pinned to the twin's storage namespace server-side, so an ordinary row can hold any URL, and letting it through produced a `/api/posts` 400 naming neither the ref nor the doc. And the image/audio/video MIME check falls back to the **filename extension** when `file_type` is `application/octet-stream`: Create Studio stores `assetMime || "application/octet-stream"` and the job's `mime_type` is nullable, so a real mp3 can arrive typeless — refusing it with "attach it with `--doc`" would be wrong advice about an attachable file.
|
|
645
|
+
- **SKILL.md**: `--media` now documents its three sources (local image path / Amiko-hosted URL / `drive:<docId>`), says a Drive **signed URL** and a `/d/<slug>` share link are both rejected, and notes that a `drive:` ref pointing at a pdf belongs on `--doc`. New Examples row for "post the audio/image from my Drive".
|
|
646
|
+
|
|
638
647
|
### 0.14.1
|
|
639
648
|
|
|
640
649
|
- **`amiko post likers <postId>`** (new): lists everyone who liked a post, paging `GET /api/posts/[id]/likes` to completeness so "who / which friends liked this" is answered from the whole set. Cross-reference the printed user ids against `amiko friends list` for the "which *friends*" variant. The route is **owner-only** (`post.user_id !== user.id` → 403), so the command says so in `--help` and turns the server's generic 403 into a message naming the rule — an agent shouldn't read it as a broken login and retry.
|
package/dist/index.js
CHANGED
|
@@ -33675,17 +33675,66 @@ function isAmikoMediaUrl(value) {
|
|
|
33675
33675
|
const parsed = new URL(value);
|
|
33676
33676
|
return parsed.protocol === "https:" && parsed.pathname.startsWith(AMIKO_PUBLIC_STORAGE_PATH);
|
|
33677
33677
|
}
|
|
33678
|
-
|
|
33678
|
+
var DRIVE_MEDIA_REF = /^drive:(.+)$/i;
|
|
33679
|
+
function isFeedMediaMime(mime) {
|
|
33680
|
+
return mime.startsWith("image/") || mime.startsWith("audio/") || mime.startsWith("video/");
|
|
33681
|
+
}
|
|
33682
|
+
var MEDIA_EXTENSIONS = new Set([
|
|
33683
|
+
"png",
|
|
33684
|
+
"jpg",
|
|
33685
|
+
"jpeg",
|
|
33686
|
+
"webp",
|
|
33687
|
+
"gif",
|
|
33688
|
+
"avif",
|
|
33689
|
+
"heic",
|
|
33690
|
+
"svg",
|
|
33691
|
+
"mp3",
|
|
33692
|
+
"m4a",
|
|
33693
|
+
"aac",
|
|
33694
|
+
"wav",
|
|
33695
|
+
"flac",
|
|
33696
|
+
"ogg",
|
|
33697
|
+
"opus",
|
|
33698
|
+
"mp4",
|
|
33699
|
+
"mov",
|
|
33700
|
+
"webm",
|
|
33701
|
+
"m4v",
|
|
33702
|
+
"mkv"
|
|
33703
|
+
]);
|
|
33704
|
+
function looksLikeMediaFilename(name) {
|
|
33705
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
33706
|
+
return MEDIA_EXTENSIONS.has(ext);
|
|
33707
|
+
}
|
|
33708
|
+
async function resolveDriveMedia(auth, docId, twinFlag) {
|
|
33709
|
+
const twinId = resolveTwinId({ flag: twinFlag, config: loadConfig(), auth });
|
|
33710
|
+
const { doc: doc2 } = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/${encodeURIComponent(docId)}`);
|
|
33711
|
+
if (!doc2?.file_url) {
|
|
33712
|
+
throw new Error(`Drive file "${docId}" not found in twin ${twinId}'s Drive (or it has no stored URL). Get a valid id from \`amiko drive list --json\` (the \`id\` field) for that same twin.`);
|
|
33713
|
+
}
|
|
33714
|
+
const mime = (doc2.file_type ?? "").toLowerCase();
|
|
33715
|
+
const filename = doc2.filename ?? "file";
|
|
33716
|
+
if (mime && !isFeedMediaMime(mime) && !looksLikeMediaFilename(filename)) {
|
|
33717
|
+
throw new Error(`Drive file "${docId}" is ${mime} (${filename}), not an image/audio/video. Attach documents with --doc, not --media.`);
|
|
33718
|
+
}
|
|
33719
|
+
if (!isAmikoMediaUrl(doc2.file_url)) {
|
|
33720
|
+
throw new Error(`Drive file "${docId}" (${filename}) is stored at a URL that isn't Amiko public storage (${doc2.file_url}), so it can't be attached to a post. Re-upload it with \`amiko drive upload\`, or pass the media's local file path to --media.`);
|
|
33721
|
+
}
|
|
33722
|
+
return doc2.file_url;
|
|
33723
|
+
}
|
|
33724
|
+
async function resolveMediaUrls(auth, inputs, opts = {}) {
|
|
33679
33725
|
const resolved = [];
|
|
33680
33726
|
for (const value of inputs) {
|
|
33681
33727
|
const trimmed = value.trim();
|
|
33682
33728
|
if (trimmed.length === 0)
|
|
33683
33729
|
continue;
|
|
33684
|
-
|
|
33730
|
+
const driveRef = trimmed.match(DRIVE_MEDIA_REF);
|
|
33731
|
+
if (driveRef) {
|
|
33732
|
+
resolved.push(await resolveDriveMedia(auth, driveRef[1].trim(), opts.twin));
|
|
33733
|
+
} else if (/^https?:\/\//i.test(trimmed)) {
|
|
33685
33734
|
if (isAmikoMediaUrl(trimmed)) {
|
|
33686
33735
|
resolved.push(trimmed);
|
|
33687
33736
|
} else {
|
|
33688
|
-
throw new Error(`"${trimmed}" is not an
|
|
33737
|
+
throw new Error(`"${trimmed}" is not an attachable media source. Use one of: a local IMAGE file path; an Amiko public storage URL (e.g. an image/music/video URL from \`amiko create\`); or \`drive:<docId>\` for a file already in your Drive (\`amiko drive list --json\`). A signed download URL or a /d/<slug> share link won't work — pass \`drive:<docId>\` instead.`);
|
|
33689
33738
|
}
|
|
33690
33739
|
} else {
|
|
33691
33740
|
resolved.push(await uploadPostMedia(auth, trimmed));
|
|
@@ -33896,7 +33945,7 @@ function registerFeedCommand(program2) {
|
|
|
33896
33945
|
});
|
|
33897
33946
|
}
|
|
33898
33947
|
function registerPostCommand(program2) {
|
|
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)
|
|
33948
|
+
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); Amiko-hosted URLs (image/music/video from `amiko create`); or `drive:<docId>` for a media file in your Drive (`amiko drive list`)").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) => {
|
|
33900
33949
|
const auth = resolveAuth();
|
|
33901
33950
|
const visibility = opts.visibility === "private" ? "private" : "public";
|
|
33902
33951
|
const content = opts.content?.trim() ?? "";
|
|
@@ -34087,7 +34136,7 @@ function registerPostCommand(program2) {
|
|
|
34087
34136
|
process.exit(1);
|
|
34088
34137
|
}
|
|
34089
34138
|
});
|
|
34090
|
-
program2.command("comment").description("Comment on a post").requiredOption("--id <postId>", "Target post id").requiredOption("--comment <text>", "Comment body").option("--media <pathOrUrl...>", "Attach media: local IMAGE file paths (uploaded automatically)
|
|
34139
|
+
program2.command("comment").description("Comment on a post").requiredOption("--id <postId>", "Target post id").requiredOption("--comment <text>", "Comment body").option("--media <pathOrUrl...>", "Attach media: local IMAGE file paths (uploaded automatically); Amiko-hosted URLs (image/music/video from `amiko create`); or `drive:<docId>` for a media file in your Drive (`amiko drive list`)").option("--twin <idOrName>", "Post as this twin (agent mode)").option("--json", "Output as JSON").action(async (opts) => {
|
|
34091
34140
|
const config2 = loadConfig();
|
|
34092
34141
|
const auth = resolveAuth();
|
|
34093
34142
|
let twinId;
|
|
@@ -34108,7 +34157,9 @@ function registerPostCommand(program2) {
|
|
|
34108
34157
|
if (opts.media?.length) {
|
|
34109
34158
|
if (spinner)
|
|
34110
34159
|
spinner.text = "Uploading media...";
|
|
34111
|
-
body.media_urls = await resolveMediaUrls(auth, opts.media
|
|
34160
|
+
body.media_urls = await resolveMediaUrls(auth, opts.media, {
|
|
34161
|
+
twin: opts.twin
|
|
34162
|
+
});
|
|
34112
34163
|
if (spinner)
|
|
34113
34164
|
spinner.text = "Posting comment...";
|
|
34114
34165
|
}
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -34,6 +34,7 @@ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar)
|
|
|
34
34
|
| "谁给我点赞了 / which friends liked my post?" | shell → `amiko post likers <postId>` (own posts only) → cross-reference ids against `amiko friends list` |
|
|
35
35
|
| "我有多少好友 / how many friends do I have?" | shell → `amiko friends list` — read the header total, don't count rows |
|
|
36
36
|
| "发个笔记 / post this photo as a note" | shell → `amiko post create --title "…" --media ./photo.webp` (no `--content` needed) |
|
|
37
|
+
| "post the audio/image from my Drive" | shell → `amiko drive list --json` (copy the `id`) → `amiko post create --title "…" --media drive:<docId>` |
|
|
37
38
|
| "share this PDF on my feed" | shell → `amiko post create --title "…" --doc ./report.pdf` |
|
|
38
39
|
| "关注 / follow @mars" | shell → `amiko users follow mars` (confirm first — it notifies them) |
|
|
39
40
|
| "what did the people I follow post?" | shell → `amiko feed --type following` |
|
|
@@ -270,8 +271,9 @@ Two different relationships — pick the one the owner actually asked for:
|
|
|
270
271
|
- image note → `amiko post create --title "Kyoto, 6am" --media ./shot.webp`
|
|
271
272
|
- document note → `amiko post create --title "Q3 report" --doc ./q3.pdf`
|
|
272
273
|
- audio with a cover image → one post, both attached: `--media ./cover.webp --media <audioUrl>` (the first media is the cover; music/video URLs from `amiko create` are accepted directly)
|
|
274
|
+
- a media file already in the twin's Drive (an upload, or a Create-Studio generation) → `--media drive:<docId>` (get the id from `amiko drive list --json`)
|
|
273
275
|
|
|
274
|
-
**Attaching documents: `--doc`, not `--media`.** `--doc <pathOrUrl...>` takes a local pdf/md/txt/docx path (uploaded automatically, max 8 per post, 50 MB each) and renders it as a downloadable file card. `--media` is images/audio/video only and rejects a document
|
|
276
|
+
**Attaching documents: `--doc`, not `--media`.** `--doc <pathOrUrl...>` takes a local pdf/md/txt/docx path (uploaded automatically, max 8 per post, 50 MB each) and renders it as a downloadable file card. `--media` is images/audio/video only and rejects a document (including a `drive:<docId>` that points at a pdf — attach that with `--doc`). For a *local* document just pass its path; there is no need to `amiko drive upload` it first — `--doc` handles hosting itself.
|
|
275
277
|
|
|
276
278
|
**@-mentions use `@[Name](userId)`, not `@handle`.** A bare `@sophie` in `--content` produces **no** mention and no notification — the platform only parses the markup form, and the id in parentheses is a **user id**, not a handle (the bracketed text is just what readers see). Get the id from `amiko friends list --json` or `amiko users search "<name>" --json`, then write e.g. `--content "thanks @[Sophie](cm1abc…) for the shots"`. The same rule applies to `amiko post comment`. (Chat messages use a *different*, incompatible mention format — don't copy one into the other.)
|
|
277
279
|
|
|
@@ -281,7 +283,12 @@ Two different relationships — pick the one the owner actually asked for:
|
|
|
281
283
|
|
|
282
284
|
**Reading a post via the CLI counts as reading it.** Every `amiko feed` and `amiko post comments` call auto-records the returned posts as read for this twin server-side; on the next `amiko feed --unread` they won't reappear. No manual "mark read" command exists. (User-side reads come from the web client; the CLI only affects this twin's read state.)
|
|
283
285
|
|
|
284
|
-
**Attaching
|
|
286
|
+
**Attaching media to a post or comment — three sources.** `--media` takes any of:
|
|
287
|
+
1. a **local file path** (images only) — `amiko post create --content "..." --media ./image.webp` — the CLI uploads it to Amiko's public post storage and attaches the returned URL. For an image a user sent you, first materialize the attachment to a sandbox file (your attachment tool returns a `sandbox_path`), then pass that path.
|
|
288
|
+
2. an **Amiko-hosted URL** — an image/music/video URL straight from `amiko create` (via `amiko create status <jobId>` / `amiko create media`). Only Amiko public-storage URLs are accepted here.
|
|
289
|
+
3. **`drive:<docId>`** — a media file already in the twin's Drive (an upload, or a Create-Studio generation mirrored there). The CLI resolves the id to the file and the **server copies it into public post storage**, so it works for image **and audio/video** alike, and the link never expires. Get the id from `amiko drive list --json` (the `id` field).
|
|
290
|
+
|
|
291
|
+
Do NOT paste a Drive **signed download URL** or a `/d/<slug>` **share link** into `--media` — those are rejected (a signed URL would also expire). Use `drive:<docId>` for anything in the Drive. And you never need the old "upload-then-paste-the-URL" dance: to attach a brand-new image, pass its local path (source 1); to attach something already in the Drive, use `drive:<docId>` (source 3).
|
|
285
292
|
|
|
286
293
|
**Sharing a post link: copy the printed URL verbatim.** `amiko post create` prints the post's canonical `URL` (`https://platform.heyamiko.com/post/<id>`; also `post_url` in `--json`). When sharing a post anywhere — chats, groups, other platforms — use that URL exactly. **Never compose a post URL yourself from the id**: guessed domains (e.g. `amiko.ai`) are not Amiko and send readers to a parked page.
|
|
287
294
|
|