Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4bdb9dc
fix: calendar get_events shows event IDs only — add summary + start t…
mayoalexander Apr 18, 2026
f485bc5
fix: calendar times — start can be string or object (#58779)
mayoalexander Apr 18, 2026
df50c11
feat: iris obs — control OBS Studio via WebSocket bridge
mayoalexander Apr 18, 2026
10fa786
test: calendar event display + fix stale test assertions (#58778/#58779)
mayoalexander Apr 18, 2026
65e2013
feat: tickets-push --force/--yes flag for automation (#58781)
mayoalexander Apr 18, 2026
73ff175
feat: iris events preflight + audit — production readiness QA
mayoalexander Apr 18, 2026
eaceaae
fix: expose update_event + delete_event in calendar CLI registry (#58…
mayoalexander Apr 18, 2026
50c1951
fix: events push/diff picks tickets file instead of event file (#5878…
mayoalexander Apr 18, 2026
0f63d1b
feat: events push pass-through — unknown fields saved to metadata (#5…
mayoalexander Apr 18, 2026
5ca3b92
feat: events push warns when fields saved to metadata
mayoalexander Apr 18, 2026
3c0a4db
fix: iMessage search default 7→30 days + --since flag for date range …
mayoalexander Apr 18, 2026
a5cb1c6
feat: iMessage resolves contact names from Leads CRM (#58888)
mayoalexander Apr 18, 2026
2cc18b6
refactor: centralize iMessage + contact resolution into shared libs
mayoalexander Apr 18, 2026
0b17e4f
refactor: platform-imessage + platform-customer → shared iMessage/con…
mayoalexander Apr 18, 2026
141c1fd
fix: imessage search resolves lead names to phone/email (#58890/#58891)
mayoalexander Apr 18, 2026
b943f9f
feat: iris events production — runsheet, checklist, budget, overview
mayoalexander Apr 18, 2026
e5500f2
feat: iris imessage contacts — read vCard contact cards from Messages…
mayoalexander Apr 18, 2026
06c33f1
fix: production command uses --event-id flag instead of positional arg
mayoalexander Apr 18, 2026
e5140c6
feat: leads create --emails for multiple email addresses
mayoalexander Apr 18, 2026
5755691
feat: iris obs dashboard — open production UI from CLI
mayoalexander Apr 18, 2026
a2cb231
feat: iris obs dashboard auto-detects ngrok, LAN IP, and localhost
mayoalexander Apr 18, 2026
f661877
fix: dashboard opens public ngrok URL instead of localhost
mayoalexander Apr 18, 2026
52ec42b
fix: bloqs create/get/ingest use wrong API route + silent error displ…
mayoalexander Apr 19, 2026
c018b22
chore: bump version to 1.2.1
mayoalexander Apr 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/opencode/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.1.25",
"version": "1.2.1",
"name": "opencode",
"displayName": "iris-agent-cli",
"type": "module",
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/cli/cmd/command-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export const COMMAND_CATEGORY_MAP: Record<string, string> = {
profile: "entities",

// Communication
obs: "communication",
phone: "communication",
voice: "communication",
transcribe: "communication",
Expand Down
37 changes: 11 additions & 26 deletions packages/opencode/src/cli/cmd/platform-atlas-comms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { irisFetch, requireAuth, handleApiError, dim, bold, success, highlight } from "./iris-api"
import { execSync } from "child_process"
import { homedir } from "os"

// ============================================================================
// Atlas Comms CLI — Unified cross-channel lead communications log
Expand Down Expand Up @@ -53,12 +51,12 @@ async function resolveLead(idOrQuery: string): Promise<{ id: number; lead: any }
return { id: leadId, lead: data?.data ?? data }
}

// ── iMessage ingestion (local SQLite) ──
// ── iMessage ingestion (via shared lib) ──

function ingestImessage(lead: any): any[] {
const MESSAGES_DB = `${homedir()}/Library/Messages/chat.db`
const { searchByHandle, normalizeHandle } = require("../lib/imessage")
const identifiers: string[] = []
if (lead.phone) identifiers.push(lead.phone.replace(/\D/g, "").slice(-10))
if (lead.phone) identifiers.push(normalizeHandle(lead.phone))
if (lead.email) identifiers.push(lead.email)
if (lead.instagram) identifiers.push(lead.instagram.replace("@", ""))

Expand All @@ -67,28 +65,15 @@ function ingestImessage(lead: any): any[] {
const items: any[] = []
for (const ident of identifiers) {
try {
const sql = `SELECT
m.rowid, m.text, m.is_from_me, m.date,
datetime(m.date/1000000000 + strftime('%s','2001-01-01'), 'unixepoch', 'localtime') as sent_dt,
c.chat_identifier
FROM message m
JOIN chat_message_join cmj ON m.rowid = cmj.message_id
JOIN chat c ON cmj.chat_id = c.rowid
WHERE c.chat_identifier LIKE '%${ident}%'
AND m.text IS NOT NULL AND m.text != ''
ORDER BY m.date DESC LIMIT 100`
const raw = execSync(`sqlite3 "${MESSAGES_DB}" "${sql}"`, { encoding: "utf-8", timeout: 10000 }).trim()
if (!raw) continue
for (const line of raw.split("\n")) {
const parts = line.split("|")
if (parts.length < 5) continue
const messages = searchByHandle(ident, 90, 100)
for (const m of messages) {
items.push({
direction: parts[2] === "1" ? "outbound" : "inbound",
from_identifier: parts[2] === "1" ? "me" : (parts[5] || ident),
body: parts[1],
sent_at: parts[4],
external_message_id: `imessage_${parts[0]}`,
metadata: { chat_identifier: parts[5] || ident },
direction: m.from_me ? "outbound" : "inbound",
from_identifier: m.from_me ? "me" : (m.chat_identifier || ident),
body: m.text,
sent_at: m.date,
external_message_id: `imessage_${m.id}`,
metadata: { chat_identifier: m.chat_identifier || ident },
})
}
} catch { /* SQLite access may fail — skip silently */ }
Expand Down
72 changes: 48 additions & 24 deletions packages/opencode/src/cli/cmd/platform-bloqs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,12 @@ const BloqsListCommand = cmd({
try {
const params = new URLSearchParams({ per_page: String(args.limit) })
const res = await irisFetch(`/api/v1/user/${userId}/bloqs?${params}`)
const ok = await handleApiError(res, "List bloqs")
if (!ok) { spinner.stop("Failed", 1); process.exitCode = 1; prompts.outro("Done"); return }
if (!res.ok) {
spinner.stop("Failed", 1)
await handleApiError(res, "List bloqs")
prompts.outro("Done")
return
}

const data = (await res.json()) as { data?: any[] }
const bloqs: any[] = data?.data ?? []
Expand Down Expand Up @@ -105,9 +109,13 @@ const BloqsGetCommand = cmd({
spinner.start("Loading…")

try {
const res = await irisFetch(`/api/v1/users/${userId}/bloqs/${args.id}`)
const ok = await handleApiError(res, "Get bloq")
if (!ok) { spinner.stop("Failed", 1); process.exitCode = 1; prompts.outro("Done"); return }
const res = await irisFetch(`/api/v1/user/${userId}/bloqs/${args.id}`)
if (!res.ok) {
spinner.stop("Failed", 1)
await handleApiError(res, "Get bloq")
prompts.outro("Done")
return
}

const data = (await res.json()) as { data?: any }
const b = data?.data ?? data
Expand All @@ -122,7 +130,7 @@ const BloqsGetCommand = cmd({
console.log()

// Load lists
const listsRes = await irisFetch(`/api/v1/users/${userId}/bloqs/${args.id}/lists`)
const listsRes = await irisFetch(`/api/v1/user/${userId}/bloqs/${args.id}/lists`)
if (listsRes.ok) {
const listsData = (await listsRes.json()) as { data?: any[] }
const lists: any[] = listsData?.data ?? []
Expand All @@ -137,7 +145,7 @@ const BloqsGetCommand = cmd({

// Load files if requested
if (args.files) {
const filesRes = await irisFetch(`/api/v1/users/${userId}/bloqs/${args.id}/files`)
const filesRes = await irisFetch(`/api/v1/user/${userId}/bloqs/${args.id}/files`)
if (filesRes.ok) {
const filesData = (await filesRes.json()) as { data?: any[] }
const files: any[] = filesData?.data ?? []
Expand Down Expand Up @@ -228,15 +236,19 @@ const BloqsCreateCommand = cmd({
spinner.start("Creating bloq…")

try {
const res = await irisFetch(`/api/v1/users/${userId}/bloqs`, {
const res = await irisFetch(`/api/v1/user/${userId}/bloqs`, {
method: "POST",
body: JSON.stringify({ name, description }),
})
const ok = await handleApiError(res, "Create bloq")
if (!ok) { spinner.stop("Failed", 1); prompts.outro("Done"); return }
if (!res.ok) {
spinner.stop("Failed", 1)
await handleApiError(res, "Create bloq")
prompts.outro("Done")
return
}

const data = (await res.json()) as { data?: any }
const b = data?.data ?? data
const data = (await res.json()) as { data?: { bloq?: any } }
const b = data?.data?.bloq ?? data?.data ?? data
spinner.stop(`${success("✓")} Bloq created: ${bold(String(b.name ?? b.id))}`)

printDivider()
Expand Down Expand Up @@ -292,14 +304,18 @@ const BloqsIngestCommand = cmd({
const formData = new FormData()
formData.append("file", new Blob([blob]), filename)

const res = await fetch(`${FL_API}/api/v1/users/${userId}/bloqs/${args.id}/files`, {
const res = await fetch(`${FL_API}/api/v1/user/${userId}/bloqs/${args.id}/files`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
body: formData,
})

const ok = await handleApiError(res, "Ingest file")
if (!ok) { spinner.stop("Failed", 1); prompts.outro("Done"); return }
if (!res.ok) {
spinner.stop("Failed", 1)
await handleApiError(res, "Ingest file")
prompts.outro("Done")
return
}

const data = (await res.json()) as { data?: any; message?: string }
spinner.stop(`${success("✓")} ${filename} ingested`)
Expand Down Expand Up @@ -380,11 +396,15 @@ const BloqsAddItemCommand = cmd({
if (title) payload.title = title

const res = await irisFetch(
`/api/v1/users/${userId}/bloqs/${args["bloq-id"]}/lists/${args["list-id"]}/items`,
`/api/v1/user/${userId}/bloqs/${args["bloq-id"]}/lists/${args["list-id"]}/items`,
{ method: "POST", body: JSON.stringify(payload) },
)
const ok = await handleApiError(res, "Add item")
if (!ok) { spinner.stop("Failed", 1); prompts.outro("Done"); return }
if (!res.ok) {
spinner.stop("Failed", 1)
await handleApiError(res, "Add item")
prompts.outro("Done")
return
}

spinner.stop(`${success("✓")} Item added`)
prompts.outro(dim(`iris bloqs get ${args["bloq-id"]}`))
Expand Down Expand Up @@ -480,21 +500,25 @@ const BloqsComposeCommand = cmd({
spinner.start("Creating knowledge base…")

try {
const res = await irisFetch(`/api/v1/users/${userId}/bloqs`, {
const res = await irisFetch(`/api/v1/user/${userId}/bloqs`, {
method: "POST",
body: JSON.stringify({ name, description }),
})
const ok = await handleApiError(res, "Create bloq")
if (!ok) { spinner.stop("Failed", 1); prompts.outro("Done"); return }
if (!res.ok) {
spinner.stop("Failed", 1)
await handleApiError(res, "Create bloq")
prompts.outro("Done")
return
}

const data = (await res.json()) as { data?: any }
const bloq = data?.data ?? data
const data = (await res.json()) as { data?: { bloq?: any } }
const bloq = data?.data?.bloq ?? data?.data ?? data
const bloqId = bloq.id

// Step 5: Create lists
let listsCreated = 0
for (const listName of suggestedLists) {
const listRes = await irisFetch(`/api/v1/users/${userId}/bloqs/${bloqId}/lists`, {
const listRes = await irisFetch(`/api/v1/user/${userId}/bloqs/${bloqId}/lists`, {
method: "POST",
body: JSON.stringify({ name: listName }),
})
Expand Down
5 changes: 2 additions & 3 deletions packages/opencode/src/cli/cmd/platform-customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,9 @@ async function searchMail(query: string, days: number): Promise<any[]> {
}

function queryMessagesDb(sql: string): string {
const { execSync } = require("child_process") as typeof import("child_process")
const db = `${process.env.HOME}/Library/Messages/chat.db`
try {
return execSync(`sqlite3 "${db}" "${sql.replace(/"/g, '\\"')}"`, { encoding: "utf-8", timeout: 5000 }).trim()
const { query } = require("../lib/imessage")
return query(sql)
} catch { return "" }
}

Expand Down
15 changes: 9 additions & 6 deletions packages/opencode/src/cli/cmd/platform-doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,15 @@ export const PlatformDoctorCommand = cmd({
// ── 5. macOS Permissions ──
sp.start("Checking macOS permissions…")
// Full Disk Access (needed for iMessage SQLite)
try {
const db = `${homedir()}/Library/Messages/chat.db`
execSync(`sqlite3 "${db}" "SELECT 1 FROM message LIMIT 1" 2>&1`, { encoding: "utf-8", timeout: 3000 })
allResults.push({ name: "Full Disk Access", ok: true, detail: "Messages.app readable" })
} catch {
allResults.push({ name: "Full Disk Access", ok: false, detail: "cannot read Messages.app", hint: "System Settings → Privacy → Full Disk Access" })
{
const { isAvailable } = await import("../lib/imessage")
const ok = isAvailable()
allResults.push({
name: "Full Disk Access",
ok,
detail: ok ? "Messages.app readable" : "cannot read Messages.app",
hint: ok ? undefined : "System Settings → Privacy → Full Disk Access",
})
}

// Contacts access (needed for address book matching)
Expand Down
Loading
Loading