Skip to content

v1.3.0 — multi-tier proposals, package management, billing fixes - #7

Merged
mayoalexander merged 15 commits into
mainfrom
dev
Apr 21, 2026
Merged

mayoalexander merged 15 commits into
mainfrom
dev

Conversation

@mayoalexander

Copy link
Copy Markdown

Summary

  • iris leads create-package — create service packages for multi-tier proposals
  • iris leads update-package — update package name, price, billing, features, scope
  • iris leads delete-gate / update-gate — manage payment gates
  • --packages 8,9,11 --term 24 on payment-gate for selectable pricing tiers
  • Feature string split no longer breaks on dollar amounts ($1,500)
  • --yes/-y flag on iris domains connect/remove
  • CSV ingest with --as items mode
  • iris invoices mark-paid for offline/cash payments
  • SOM toggle + status commands

Bugs fixed

  • #61417: Wrong billing type, hardcoded 12mo, comma split
  • #61423: update-package missing from CLI
  • #61429: Feature split regex breaks on ,30
  • #61319: Package creation validation errors

Test plan

  • iris leads create-package 40 -n "Test" -a 100 -b monthly creates package
  • iris leads update-package 40 <id> --billing one_time updates package
  • iris leads payment-gate <id> --packages 8,9,11 --term 24 creates multi-tier gate
  • iris leads delete-gate <id> deletes gate
  • Feature split preserves $1,500 but splits Feature A, Feature B

🤖 Generated with Claude Code

mayoalexander and others added 15 commits April 19, 2026 13:09
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>
Copilot AI review requested due to automatic review settings April 21, 2026 03:42
@mayoalexander
mayoalexander merged commit 9ccd366 into main Apr 21, 2026
1 of 6 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-paid for offline payments; add --yes/-y to domains connect/remove; add SOM toggle/status; improve diary scoping to support user-level diaries by default.
  • Add CSV ingest modes (dataset vs items) 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.

Comment on lines +19 to +24
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]

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +416 to +418

if (args.state === "on") newState = true
else if (args.state === "off") newState = false

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.2.4",
"version": "1.2.5",

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +2280 to +2287
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

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +435 to +440
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 }

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
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)`) : ""}`)

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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)`) : ""}`)

Copilot uses AI. Check for mistakes.
Comment on lines +2221 to +2222
if (args.packages) body.package_ids = args.packages.split(",").map(Number)
if (args.interval) body.interval = args.interval

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--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.

Copilot uses AI. Check for mistakes.
Comment on lines 2198 to 2201
.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" })

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
mayoalexander added a commit that referenced this pull request Sep 14, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants