Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions packages/opencode/src/cli/cmd/command-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ export const COMMAND_CATEGORY_MAP: Record<string, string> = {
workflows: "agents",
schedules: "agents",
monitor: "agents",
personality: "agents",
recall: "knowledge",

// Integrations & Tools
integrations: "integrations",
Expand Down
116 changes: 116 additions & 0 deletions packages/opencode/src/cli/cmd/platform-personality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { irisFetch, requireAuth, requireUserId, handleApiError, printDivider, dim, bold, success } from "./iris-api"

// Personality presets — list available preset library and apply to an agent.
// Backed by config/personalities.php (fl-api).
//
// Usage:
// iris personality # list presets
// iris personality show concise # show full preset traits
// iris personality apply <agent_id> concise

const PersonalityListCommand = cmd({
command: "list",
aliases: ["ls"],
describe: "list available personality presets",
builder: (yargs) => yargs.option("json", { type: "boolean", default: false }),
async handler(args) {
UI.empty()
prompts.intro("◈ Personality Presets")
const token = await requireAuth(); if (!token) { prompts.outro("Done"); return }
const res = await irisFetch(`/api/v1/personalities`)
const ok = await handleApiError(res, "List personalities"); if (!ok) { prompts.outro("Done"); return }
const data = (await res.json()) as any
const presets: any[] = data?.data ?? []
if (args.json) { console.log(JSON.stringify(presets, null, 2)); prompts.outro("Done"); return }
printDivider()
for (const p of presets) {
console.log(` ${bold(p.key.padEnd(12))} ${p.name}`)
console.log(` ${dim("".padEnd(12))} ${dim(p.description)}`)
console.log()
}
printDivider()
prompts.outro(`${presets.length} preset(s) — apply with: iris personality apply <agent_id> <key>`)
},
})

const PersonalityShowCommand = cmd({
command: "show <key>",
describe: "show full traits text for a preset",
builder: (yargs) =>
yargs
.positional("key", { type: "string", demandOption: true, describe: "preset key (e.g. concise, formal)" })
.option("json", { type: "boolean", default: false }),
async handler(args) {
UI.empty()
prompts.intro(`◈ Preset: ${bold(args.key as string)}`)
const token = await requireAuth(); if (!token) { prompts.outro("Done"); return }
const res = await irisFetch(`/api/v1/personalities/${encodeURIComponent(String(args.key))}`)
const ok = await handleApiError(res, "Show personality"); if (!ok) { prompts.outro("Done"); return }
const data = (await res.json()) as any
const preset = data?.data
if (!preset) { prompts.outro("Not found"); return }
if (args.json) { console.log(JSON.stringify(preset, null, 2)); prompts.outro("Done"); return }
printDivider()
console.log(` ${bold("Name:")} ${preset.name}`)
console.log(` ${bold("Description:")} ${preset.description}`)
console.log()
console.log(` ${bold("Traits:")}`)
console.log(` ${preset.traits}`)
printDivider()
prompts.outro("Done")
},
})

const PersonalityApplyCommand = cmd({
command: "apply <agentId> [key]",
describe: "apply a preset (or raw traits via --traits) to an agent",
builder: (yargs) =>
yargs
.positional("agentId", { type: "string", demandOption: true, describe: "target bloq_agent ID" })
.positional("key", { type: "string", describe: "preset key (omit when using --traits)" })
.option("traits", { type: "string", describe: "raw personality_traits text (alternative to a preset key)" })
.option("json", { type: "boolean", default: false }),
async handler(args) {
if (!args.key && !args.traits) {
console.error("personality apply: provide either a preset key or --traits \"...\"")
process.exit(1)
}
UI.empty()
prompts.intro(`◈ Apply Personality → agent ${bold(String(args.agentId))}`)
const token = await requireAuth(); if (!token) { prompts.outro("Done"); return }
const userId = await requireUserId(); if (!userId) { prompts.outro("Done"); return }

const body: Record<string, string> = {}
if (args.key) body.preset = String(args.key)
if (args.traits) body.traits = String(args.traits)

const res = await irisFetch(`/api/v1/users/${userId}/bloqs/agents/${args.agentId}/apply-personality`, {
method: "POST",
body: JSON.stringify(body),
})
const ok = await handleApiError(res, "Apply personality"); if (!ok) { prompts.outro("Done"); return }
const data = (await res.json()) as any
if (args.json) { console.log(JSON.stringify(data, null, 2)); prompts.outro("Done"); return }
printDivider()
console.log(` ${success("✓")} ${data?.message ?? "Applied"}`)
if (data?.data?.applied_from) console.log(` ${dim("From:")} ${data.data.applied_from}`)
printDivider()
prompts.outro("Done")
},
})

export const PlatformPersonalityCommand = cmd({
command: "personality <command>",
aliases: ["personalities"],
describe: "manage agent personality presets — list, show, apply",
builder: (yargs) =>
yargs
.command(PersonalityListCommand)
.command(PersonalityShowCommand)
.command(PersonalityApplyCommand)
.demandCommand(1, "specify a subcommand: list | show | apply"),
handler: () => {},
})
81 changes: 81 additions & 0 deletions packages/opencode/src/cli/cmd/platform-recall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { irisFetch, requireAuth, handleApiError, printDivider, dim, bold, IRIS_API } from "./iris-api"

// Cross-source recall — search past sessions, agent memory, and diary.
// Backed by /api/v6/recall on iris-api.
//
// Usage:
// iris recall "saddlepass deal"
// iris recall "vagaro integration" --days 30 --no-summarize
// iris recall "carrington" --agent 11 --limit 10

export const PlatformRecallCommand = cmd({
command: "recall <query..>",
aliases: ["search-memory"],
describe: "search past sessions, memory, and diary for a query",
builder: (yargs) =>
yargs
.positional("query", { describe: "what to search for", type: "string", array: true })
.option("days", { type: "number", default: 14, describe: "lookback window for diary (1-90)" })
.option("limit", { type: "number", default: 5, describe: "max hits per source (1-20)" })
.option("agent", { type: "string", describe: "scope to a specific agent_id" })
.option("summarize", { type: "boolean", default: true, describe: "LLM summary of hits (gpt-5-nano)" })
.option("json", { type: "boolean", default: false, describe: "raw JSON output" }),
async handler(args) {
const query = (args.query ?? []).join(" ").trim()
if (!query) {
console.error("recall: query is required — e.g. iris recall \"the saddlepass deal\"")
process.exit(1)
}
const token = await requireAuth(); if (!token) return

UI.empty()
prompts.intro(`◈ Recall: ${bold(query)}`)

const qs = new URLSearchParams({
q: query,
days: String(args.days),
limit: String(args.limit),
summarize: args.summarize ? "1" : "0",
})
if (args.agent) qs.set("agent_id", args.agent)

const res = await irisFetch(`/api/v6/recall?${qs.toString()}`, {}, IRIS_API)
const ok = await handleApiError(res, "Recall")
if (!ok) { prompts.outro("Done"); return }

const data = (await res.json()) as any

if (args.json) {
console.log(JSON.stringify(data, null, 2))
prompts.outro("Done")
return
}

if (data?.summary) {
printDivider()
console.log(` ${bold("Summary")}`)
console.log(` ${data.summary}`)
}

const renderBucket = (label: string, items: any[]) => {
if (!items?.length) return
printDivider()
console.log(` ${bold(label)} ${dim(`(${items.length})`)}`)
for (const item of items) {
const when = item.created_at ?? item.date ?? ""
const text = (item.content ?? item.summary ?? "").toString().slice(0, 200)
console.log(` ${dim(when)} ${text}`)
}
}

renderBucket("Memory", data?.hits?.memory ?? [])
renderBucket("Chat", data?.hits?.chat ?? [])
renderBucket("Diary", data?.hits?.diary ?? [])

printDivider()
prompts.outro(`${data?.count ?? 0} hit(s) for "${query}"`)
},
})
15 changes: 13 additions & 2 deletions packages/opencode/src/cli/cmd/platform-sdk-call.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { irisFetch, requireAuth, requireUserId, handleApiError, printDivider, dim, bold } from "./iris-api"
import { irisFetch, requireAuth, requireUserId, handleApiError, printDivider, dim, bold, FL_API, IRIS_API } from "./iris-api"

// ============================================================================
// platform-sdk-call — generic SDK proxy
Expand All @@ -27,6 +27,8 @@ interface RouteDescriptor {
path: string
// params consumed by URL placeholders are removed; remaining go to body (POST/PUT/PATCH) or query (GET/DELETE)
needsUserId?: boolean
// which API to call — defaults to fl-api. iris-api endpoints (e.g. /api/v6/recall) set this to "iris"
base?: "fl" | "iris"
}

// Endpoints sourced directly from PHP SDK Resources/* (grepped April 2026)
Expand Down Expand Up @@ -116,6 +118,14 @@ const ROUTES: Record<string, RouteDescriptor> = {
"tools.list": { method: "GET", path: "/api/v1/tools" },
"tools.invoke": { method: "POST", path: "/api/v1/tools/invoke" },

// Personality presets (fl-api)
"personalities.list": { method: "GET", path: "/api/v1/personalities" },
"personalities.get": { method: "GET", path: "/api/v1/personalities/{key}" },
"personalities.apply": { method: "POST", path: "/api/v1/users/{userId}/bloqs/agents/{agent}/apply-personality", needsUserId: true },

// Cross-source recall (iris-api)
"recall.search": { method: "GET", path: "/api/v6/recall", base: "iris" },

// Bloq ingestion
"bloqs.ingestFolder": { method: "POST", path: "/api/v1/bloqs/{bloqId}/ingest-folder" },
"bloqs.ingestionJobs": { method: "GET", path: "/api/v1/bloqs/{bloqId}/ingestion-jobs" },
Expand Down Expand Up @@ -301,10 +311,11 @@ export const PlatformSdkCallCommand = cmd({
body = JSON.stringify(remaining)
}

const apiBase = route.base === "iris" ? IRIS_API : FL_API
const res = await irisFetch(finalUrl, {
method: route.method,
...(body ? { body } : {}),
})
}, apiBase)
Comment on lines +314 to +318
const ok = await handleApiError(res, `${route.method} ${endpoint}`)
if (!ok) process.exit(1)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,46 @@ export function Autocomplete(props: {
}
}

const insertSlash = (name: string) => () => {
const newText = "/" + name + " "
const cursor = props.input().logicalCursor
props.input().deleteRange(0, 0, cursor.row, cursor.col)
props.input().insertText(newText)
props.input().cursorOffset = Bun.stringWidth(newText)
}
Comment on lines +377 to +383

results.push(
{
display: "/new",
aliases: ["/clear"],
description: "create a new session",
onSelect: () => command.trigger("session.new"),
},
{
display: "/recall",
description: "search past sessions, memory, and diary",
onSelect: insertSlash("recall"),
},
{
display: "/personality",
description: "view or switch agent personality preset",
onSelect: insertSlash("personality"),
},
{
display: "/usage",
description: "show token usage and costs",
onSelect: insertSlash("usage"),
},
{
display: "/insights",
description: "show usage insights over time (e.g. /insights 7)",
onSelect: insertSlash("insights"),
},
{
display: "/sdk",
description: "invoke any SDK endpoint (e.g. /sdk leads.list search=acme)",
onSelect: insertSlash("sdk"),
},
{
display: "/models",
description: "list models",
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ import { PlatformToolsCommand } from "./cli/cmd/platform-tools"
import { PlatformUsersCommand } from "./cli/cmd/platform-users"
import { PlatformPhoneCommand } from "./cli/cmd/platform-phone"
import { PlatformVoiceCommand } from "./cli/cmd/platform-voice"
import { PlatformRecallCommand } from "./cli/cmd/platform-recall"
import { PlatformPersonalityCommand } from "./cli/cmd/platform-personality"
import { PlatformMailCommand } from "./cli/cmd/platform-mail"
import { PlatformImessageCommand } from "./cli/cmd/platform-imessage"
import { PlatformCalendarCommand } from "./cli/cmd/platform-calendar"
Expand Down Expand Up @@ -278,6 +280,8 @@ const cli = yargs(rawArgs)
.command(reg(PlatformUsersCommand))
.command(reg(PlatformPhoneCommand))
.command(reg(PlatformVoiceCommand))
.command(reg(PlatformRecallCommand))
.command(reg(PlatformPersonalityCommand))
.command(reg(PlatformMailCommand))
.command(reg(PlatformImessageCommand))
.command(reg(PlatformCalendarCommand))
Expand Down
14 changes: 14 additions & 0 deletions scaffold/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ When the user asks something that might match a recipe, **read the recipe file f
- **NEVER invent component type names.** Run `iris pages component-registry` first. Invalid types render blank.
- **READ CLI output carefully.** Use exact values shown — don't make up IDs, URLs, or status values.

## In-chat slash commands

When the user's message starts with one of these slash commands, treat it as a structured request and respond using `iris sdk:call` (preferred) or the appropriate `iris` shell command — don't ask follow-up questions if the intent is clear.

| Command | What it means | How to handle |
|---|---|---|
| `/recall <query>` | Search past sessions, memory, and diary for the query | Use `iris sdk:call diary.list` and `iris memory show <bloq>` to gather context, then summarize matches. If no specific bloq is set, search across the user's recent diary entries. |
| `/personality [name]` | View or switch the active agent's personality | No name → list available agents with their `personality_traits` via `iris sdk:call agents.list userId=me`. With a name → find a matching agent or update the active agent's `personality_traits` field via `iris agents push` after pulling. |
Comment on lines +66 to +67
| `/usage` | Show token usage and costs | Run `iris stats` and surface the totals. Show recent session breakdown if available. |
| `/insights [days]` | Usage insights over a time range | Default 7 days. Use `iris stats` plus `iris sdk:call diary.list days=<N>` to show token consumption + agent activity over the window. |
| `/sdk <resource.method> [params]` | Call any IRIS SDK endpoint directly | Run `iris sdk:call <resource.method> <key=value>...` via bash. If the user picks `/sdk` without args, show categories from `iris sdk:call --list` so they can choose. |

These slash messages are user shortcuts — interpret them, do the work, return a concise result. Don't echo the slash back; just answer.

## Genesis Page Builder — Component Rules

When building or editing pages with `iris pages`, follow these rules:
Expand Down
Loading