v1.3.0 — multi-tier proposals, package management, billing fixes - #7
Conversation
Removed mandatory agentId positional from all subcommands. Three scopes: iris diary → user's personal diary (default) iris diary --agent 11 → agent-level diary iris diary --bloq 325 → project-level diary Better output: scope labels, bloq/agent names, heartbeat indicators, section counts, add command hint in footer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users report iris --version showing old version after successful upgrade. This is caused by shell hash caching the old binary path. Added explicit guidance to run hash -r after upgrade. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s get null guard - Add iris leads update-gate <id> and delete-gate <id> commands - Default leads list hides Prospected, sorts by status priority (Won first) - Add --all flag to show all leads including Prospected - iMessage pulse falls back to contact name search when no phone/email - bloqs get handles null API response gracefully Fixes #60082, #60087, #60089 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add --bloq alias to iris leads update (was only --bloq-id) - Normalize severity to lowercase before API call to prevent validation 422 Fixes #60106, #60107 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…#60084) Supports cash, check, wire, ach, zelle, venmo, paypal, crypto, barter, other. Usage: iris invoices mark-paid <lead-id> --amount 100 --method cash Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
iris bloqs ingest now detects .csv files and parses them into a single bloq item with type=dataset containing headers, row_count, and all rows as structured JSON. Previews first 3 rows before saving. Non-CSV files still upload as cloud file attachments (existing behavior). Use case: Patsy's Poshmark inventory CSV → one searchable bloq item with all products as structured data. Individual rows can be expanded to full bloq items later if needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ction - --as dataset (default): single bloq item with all rows as JSON - --as items: one bloq item per CSV row, title auto-detected from name-like columns - --key <column>: upsert dedup on re-import — matches existing items by key, updates in place instead of creating duplicates - --title-column <column>: override auto-detected title column - --list <name>: target a specific bloq list Tested: 8-row Poshmark inventory CSV → 8 items created (first import), then re-imported with --key Item → 0 created, 8 updated (perfect dedup). 18 unit tests for parser, title detection, and dedup logic. Fixes #60128 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
iris-platform.md: Backend-in-a-box setup — bloq CRUD, AI chat, staging subdomains, proxy mode, client onboarding checklist, troubleshooting. event-production.md: Full event playbook — tickets, timed windows, QR check-in, door sales, OBS, production management. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add --interval, --term, --deposit, --list-price, --discount, --packages flags to `iris leads payment-gate`. Matches API fields shipped in #61099/#61100. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New command to create service packages for multi-tier proposals: iris leads create-package <bloq> -n "Starter" -a 250 -b monthly -s "Basic plan" iris leads create-package <bloq> -n "Pro" -a 750 -b monthly -f "Priority,Analytics" Aliases: add-package, new-package Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skip interactive confirmation prompt for scripted/automated use. Fixes #61317 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… (#61417)
Split regex changed from split(",") to split(/,(?!\d)/) so commas inside
dollar amounts (thousands separators) are preserved as part of the feature string.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PATCH /bloqs/{bloqId}/packages/{id} with --name, --price, --billing,
--scope, --features, --active flags. Same comma-safe split as create-package.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… digit-after-comma (#61429)
Changed from ,(?!\d) to ,(?!\d{3}(?!\d)) so commas followed by
non-thousands digits (e.g. ",30 days") still split correctly while
"$1,500" stays intact.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
iris som status — show all campaigns on/off iris som toggle <campaign> [on|off] — flip a campaign, shows new state Reads/writes som-config.js active flag directly. No API needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR expands the iris CLI with new lead/proposal package management, payment gate management, offline invoice payment recording, and SOM controls, alongside new operational “how-to” docs and several workflow fixes (CSV ingest, diary scoping, upgrade messaging).
Changes:
- Add leads commands for creating/updating service packages and for updating/deleting payment gates; enhance lead listing and pulse iMessage lookup behavior.
- Add invoices
mark-paidfor offline payments; add--yes/-yto domains connect/remove; add SOMtoggle/status; improve diary scoping to support user-level diaries by default. - Add CSV ingest modes (
datasetvsitems) and include new platform/how-to documentation.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| scaffold/how-to/iris-platform.md | New guide for using IRIS as a backend + staging proxy pattern. |
| scaffold/how-to/event-production.md | New event production playbook and CLI quick reference. |
| packages/opencode/src/cli/cmd/upgrade.ts | Improve post-upgrade guidance for shell hash caching. |
| packages/opencode/src/cli/cmd/platform-som.ts | Add SOM campaign toggle/status commands. |
| packages/opencode/src/cli/cmd/platform-leads.ts | Add/extend leads commands (packages, gates), adjust list filtering/sorting, improve pulse iMessage behavior. |
| packages/opencode/src/cli/cmd/platform-invoices.ts | Add mark-paid for offline/cash payments. |
| packages/opencode/src/cli/cmd/platform-domains.ts | Add --yes/-y to skip confirmations for connect/remove. |
| packages/opencode/src/cli/cmd/platform-diary.ts | Rework diary to be user-scoped by default; add agent/bloq scoping options. |
| packages/opencode/src/cli/cmd/platform-bug.ts | Normalize severity casing before submission. |
| packages/opencode/src/cli/cmd/platform-bloqs.ts | Add CSV parsing + ingest modes (dataset / items) and additional ingest options. |
| packages/opencode/package.json | Version bump to 1.2.5. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function getSdkUserId(): string | undefined { | ||
| const envPath = join(process.env.HOME || "~", ".iris", "sdk", ".env") | ||
| if (existsSync(envPath)) { | ||
| const content = readFileSync(envPath, "utf-8") | ||
| const match = content.match(/IRIS_USER_ID=(\d+)/) | ||
| if (match) return match[1] |
There was a problem hiding this comment.
getSdkUserId() builds the path with join(process.env.HOME || "~", ...). If HOME is unset (common on Windows; also possible in some shells), "~" is not expanded by Node, so the lookup will always fail and user-level diary requests won’t include user_id. Prefer os.homedir() (as used elsewhere in the CLI) to reliably locate ~/.iris/sdk/.env cross-platform.
|
|
||
| if (args.state === "on") newState = true | ||
| else if (args.state === "off") newState = false |
There was a problem hiding this comment.
state is compared against literal "on"/"off" without normalization. Inputs like ON, Off, etc. will currently be treated as “toggle”, which is surprising for a command that documents explicit states. Consider lowercasing args.state (or using choices: ["on","off"]) before branching.
| if (args.state === "on") newState = true | |
| else if (args.state === "off") newState = false | |
| const normalizedState = typeof args.state === "string" ? args.state.toLowerCase() : args.state | |
| if (normalizedState === "on") newState = true | |
| else if (normalizedState === "off") newState = false |
| { | ||
| "$schema": "https://json.schemastore.org/package.json", | ||
| "version": "1.2.4", | ||
| "version": "1.2.5", |
There was a problem hiding this comment.
The PR title/description references a v1.3.0 release, but packages/opencode/package.json only bumps to 1.2.5. If this PR is intended to be the 1.3.0 release, the version should be updated accordingly (or adjust the PR metadata/changelog expectations).
| if (!args.amount && !args.scope) { | ||
| prompts.log.error("Provide at least --amount or --scope to update") | ||
| return | ||
| } | ||
|
|
||
| const body: Record<string, unknown> = {} | ||
| if (args.amount) body.amount = args.amount | ||
| if (args.scope) body.scope = args.scope |
There was a problem hiding this comment.
In update-gate, the presence checks use truthiness (if (!args.amount && !args.scope) and if (args.amount) ...). This will treat --amount 0 as “not provided” and omit it from the request body. If 0 should be a valid value (or just for consistency with other commands), switch to null/undefined checks (e.g., args.amount != null).
| if (!args.amount && !args.scope) { | |
| prompts.log.error("Provide at least --amount or --scope to update") | |
| return | |
| } | |
| const body: Record<string, unknown> = {} | |
| if (args.amount) body.amount = args.amount | |
| if (args.scope) body.scope = args.scope | |
| if (args.amount == null && args.scope == null) { | |
| prompts.log.error("Provide at least --amount or --scope to update") | |
| return | |
| } | |
| const body: Record<string, unknown> = {} | |
| if (args.amount != null) body.amount = args.amount | |
| if (args.scope != null) body.scope = args.scope |
| let toCreate = rows | ||
| let toUpdate: { item: any; row: Record<string, string> }[] = [] | ||
| if (keyCol && existingItems.length > 0) { | ||
| for (const row of rows) { | ||
| const keyVal = row[keyCol] | ||
| if (!keyVal) { toCreate.push(row); continue } |
There was a problem hiding this comment.
In CSV ingest --as items mode, toCreate is initialized as rows and then rows with a missing key are pushed back into toCreate inside the loop. Since toCreate and rows are the same array, this mutates the source array and can duplicate rows / inflate the create count. Initialize toCreate separately (e.g., start with an empty array or use a filtered copy) and avoid pushing into the rows array while iterating.
| let toCreate = rows | |
| let toUpdate: { item: any; row: Record<string, string> }[] = [] | |
| if (keyCol && existingItems.length > 0) { | |
| for (const row of rows) { | |
| const keyVal = row[keyCol] | |
| if (!keyVal) { toCreate.push(row); continue } | |
| let toCreate = rows.slice() | |
| let toUpdate: { item: any; row: Record<string, string> }[] = [] | |
| if (keyCol && existingItems.length > 0) { | |
| for (const row of rows) { | |
| const keyVal = row[keyCol] | |
| if (!keyVal) continue |
| leads = leads.slice(0, args.limit) | ||
| const total = data?.meta?.total ?? leads.length | ||
| spinner.stop(`${total} lead(s)`) | ||
| spinner.stop(`${leads.length} lead(s)${!args.all && !args.status ? dim(` (${total} total — use --all to see Prospected)`) : ""}`) |
There was a problem hiding this comment.
The summary message after listing leads appends "use --all to see Prospected" when !args.all && !args.status, but Prospected leads are only hidden when !args.all && !args.status && !args.search. With --search this message can be misleading because no filtering happened. Consider including !args.search in the condition (or basing the message on whether the Prospected filter was actually applied).
| spinner.stop(`${leads.length} lead(s)${!args.all && !args.status ? dim(` (${total} total — use --all to see Prospected)`) : ""}`) | |
| spinner.stop(`${leads.length} lead(s)${!args.all && !args.status && !args.search ? dim(` (${total} total — use --all to see Prospected)`) : ""}`) |
| if (args.packages) body.package_ids = args.packages.split(",").map(Number) | ||
| if (args.interval) body.interval = args.interval |
There was a problem hiding this comment.
--packages parsing uses args.packages.split(",").map(Number), which will produce 0 for empty segments and NaN for non-numeric values (and doesn't trim whitespace). This can send invalid IDs to the API. Consider splitting, trimming, filtering out empty strings, validating all are finite numbers, and failing fast with a clear error if any are invalid.
| .positional("id", { describe: "lead ID", type: "number", demandOption: true }) | ||
| .option("amount", { alias: "a", describe: "total amount", type: "number", demandOption: true }) | ||
| .option("scope", { alias: "s", describe: "scope of work", type: "string", demandOption: true }) | ||
| .option("bloq", { alias: "b", describe: "bloq ID", type: "number" }) |
There was a problem hiding this comment.
PR description/test plan shows iris leads payment-gate <id> --packages 8,9,11 --term 24 working without --amount/--scope, but the command still marks both amount and scope as demandOption: true. As written, the example invocation will fail argument validation. Either update the docs/test plan or make --amount/--scope optional when --package/--packages is provided (and enforce the correct combinations).
S1 of #185277 (epic anomalyco#503, list anomalyco#1688). "Hive Rooms" (#184778) asked for six new commands against a room system that has been live since July; four already existed, and join/leave existed too — as API routes (`POST`/`DELETE /api/threads/{id}/agents`) the CLI had simply never exposed. THE IDENTITY DECISION, MEASURED RATHER THAN DESIGNED `agent_thread_participants` has two types, `user` and `agent`, and `agent_id` is a free string — `AgentThread::addAgent()` writes `participant_type = agent` whatever you hand it. So before writing anything I posted `{agent_id:"node:<uuid>", role:"observer"}` at the live API: 200, and the participant appears in the thread. Removing it with the id URL-encoded: 200, and it is gone. A machine therefore joins as an agent-typed participant with a `node:`-prefixed id. No migration, no new participant type, no external-agent credentials. GAP #7 on that epic already recorded `compute_nodes.agent_ids ↔ room participant agent_id` as a derivable link; this is that link, made real. The prefix is load-bearing: a bloq agent's id is an int rendered as a string ("243" is a real participant in a live room), so a bare UUID would share the field with nothing to tell the kinds apart. WHAT THE TESTS ENCODE (17, all from measurement) - `node:` is idempotent — prefixing twice would yield `node:node:…` - an empty id THROWS rather than producing the bare prefix, which would be a participant that matches nothing and deletes nothing - the id must be URL-ENCODED for removal: `node:` carries a COLON into a URL path, and unencoded the answer reads as "not a participant" rather than "your URL was wrong" - exactly one of --agent/--node/--this-node, or refuse: the wrong machine silently joining a room and receiving its messages is not something a person watching that room would notice, so guessing is worse than stopping - role is validated here, so a typo is a sentence and not a 422 blob - an un-enrolled machine names the fix (`iris hive connect`) ONE BUG THIS FOUND IN ITSELF: the first version omitted the third argument to `irisFetch`, which defaults to FL_API while threads live on IRIS_API. It returned 404 WITH valid auth — indistinguishable from "no such room or participant". Now passes IRIS_API explicitly, with the reason recorded at the call site. VERIFIED LIVE against a real room: joined as `node:01a09d2c-…(observer)` alongside the existing `243(primary)`, then removed; participant list confirmed before, between and after. Refusal paths checked too (no selector, two selectors, bad role). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LdrjmbYDWxNnhAeH134A16
Summary
iris leads create-package— create service packages for multi-tier proposalsiris leads update-package— update package name, price, billing, features, scopeiris leads delete-gate/update-gate— manage payment gates--packages 8,9,11 --term 24onpayment-gatefor selectable pricing tiers--yes/-yflag oniris domains connect/remove--as itemsmodeiris invoices mark-paidfor offline/cash paymentsBugs fixed
,30Test plan
iris leads create-package 40 -n "Test" -a 100 -b monthlycreates packageiris leads update-package 40 <id> --billing one_timeupdates packageiris leads payment-gate <id> --packages 8,9,11 --term 24creates multi-tier gateiris leads delete-gate <id>deletes gate$1,500but splitsFeature A, Feature B🤖 Generated with Claude Code