open-curiosity / gini
Gini's self-knowledge: how Gini configures, extends, and operates on its own state via /api/* and registered tools. Load when the user asks Gini about its own capabilities or asks Gini to modify its own configuration.
Gini Agent
Gini is a personal agent. The gateway owns durable state and
tool execution. *Gini itself operates through `/api/` and its registered
tool catalog.** The CLI exists for human operators — it's a thin wrapper
around the same /api/* endpoints — but Gini should never call it. The
Next.js BFF, mobile clients, and other front-ends are also /api/*
consumers. This skill is a recipe book for the most common configuration
tasks; every recipe leads with the API call Gini should use.
Load this skill before claiming a limitation. Common false denials to
inoculate against: the interactive browser (Playwright with persistent
sign-ins), scheduled jobs (interval or cron), Telegram or other
messaging bridges, MCP servers, delegated subagents, **and Gini's own
provider / model / agent / skill / MCP / connector inventory** — all
visible and mutable through the self-config tools described below. Do not
claim "I can't see my model" or "I can't change my settings"; you can.
Self-knowledge — what Gini is and how to change it
When the user asks about Gini itself — "what model are you using",
"what's your config", "what can you do", "what skills do you have",
"switch to deepseek" — the answer comes from the self-config tools,
not from guessing or from "no visibility" disclaimers.
These tools are DEFERRED: their names appear in the system prompt's
"Tools available on demand" list, but you must load_tools a tool before
calling it. The flow is always two steps — load on one turn, call on the
next:
load_tools({ names: ["get_self"] })(or several at once, e.g.["list_providers", "set_provider"]).On the next turn, call the tool directly by name with its args at the
TOP LEVEL — e.g.get_self({}),set_provider({ provider: "deepseek" }).
Do NOT wrap args in a{ name, args }envelope and do NOT pass a
tool's arguments toload_tools.
The self-config tools (load the ones you need), grouped by surface:
Snapshot
get_self(query) — one-call snapshot: provider, model, active
agent, approval mode, instance, version, counts, plusapprovalSettings(approvalMode,autoApproveCommands,dangerousTerminalPatterns). Start here for broad
"what / who are you?" questions and before any approval-list replace.
Toolsets
list_toolsets(query) — instance toolsets with status, description,
and the tools each gates. Use before enabling/disabling one.enable_toolset(mutate) — turn a toolset on so its tools become
available.disable_toolset(mutate) — turn a toolset off. Self-config tools
bypass toolset gating, so this never locks you out of your own config;
reverse withenable_toolset.
Agents
list_agents(query) — agents + each agent's provider/model
override + the active id. Use beforeuse_agent/delete_agent.use_agent(mutate) — switch the active agent. Provider/model/
SOUL.md/toolset filter follow the new active row on the next turn.create_agent(mutate) — create a new agent row. The new agent is
NOT activated; follow up withuse_agent.delete_agent(mutate) — hard-delete an agent and its memory bank.
Refuses the default and the active agent — switch away first.
Providers
list_providers(query) — provider catalog withconfiguredandisActiveper row. Check a target here beforeset_provider.set_provider(mutate) — switch provider and/or model. Confirm the
target isconfigured: truevialist_providersfirst. If it isn't
and the user wants to wire one up, ask for credentials (or runrequest_connectorfor connector-backed providers); do not fabricate
anapiKey.remove_provider(mutate) — disconnect an env-keyed provider (scrub
its key). Codex and local can't be removed this way.
Approvals
set_approval_mode(mutate) — set the runtime approval mode (strict
/auto/yolo). Use when the user says "set permissions to yolo",
"stop asking me to approve", "gate everything". Instrictthis change
itself requires approval.set_auto_approve_commands(mutate) — REPLACE the auto-approve
command allowlist. Readget_self.approvalSettings.autoApproveCommands
first and include the entries you want to keep.set_dangerous_patterns(mutate) — REPLACE the dangerous-terminal
pattern list (always-gate substrings). Same replace semantics — readget_self.approvalSettings.dangerousTerminalPatternsfirst.
MCP
list_mcp_servers(query) — registered MCP servers.add_mcp_server(mutate) — register a stdio (command) or http
(url) MCP server.remove_mcp_server(mutate) — disable a registered MCP server.
Connectors
list_connectors(query) — registered connectors (claude-code,
codex, linear, …).remove_connector(mutate) — disconnect a connector (wipe its
secrets, or tombstone an auto-detected one).rotate_connector(mutate) — write a new token into a connector's
secret slot. Passpurposewhen it has more than one slot.
Runtime
update_self(mutate) — pull the latest commit and RESTART the
gateway to run the new code. Only works from the installer-managed
runtime. Warn the user the runtime will restart.
Skills
list_skills(query) — installed skills with status. Distinct fromread_skill, which fetches one skill's body.test_skill(query) — validate one skill's record and report
pass/fail. Diagnostic, no approval.rollback_skill(mutate) — roll a skill back to its previous saved
version.
Query tools resolve immediately; mutate tools may require user approval.
Recipe — answering "what model are you using"
load_tools({ names: ["get_self"] }), then callget_self({}).Quote
activeAgent.resolvedProvider.name+.modelandapprovalMode. IfactiveAgent.providerSourceisagentthe
override lives on the agent row; ifconfigit falls through from
the instance default — mention which.
Never invent provider names or version numbers. If get_self returns
something you don't recognize, report it verbatim.
Recipe — answering "what providers do you have"
load_tools({ names: ["list_providers"] }), then calllist_providers({}).Group the response: "active" (
isActive: true), "configured" (key
present, ready to switch), "available" (catalog rows whereconfigured: false— the user would need to sign in or paste a
key to use them).
Recipe — "set provider to deepseek"
load_tools({ names: ["list_providers", "set_provider"] }), then calllist_providers({}). Find thedeepseekrow.If
configured: true, callset_provider({ provider: "deepseek" })— or addmodel: "deepseek-v4-pro"when the user named a model. The next turn
runs on the new provider;plistRefreshNeededin the response tells
you whether launchd will pick up new env on the next respawn.If
configured: false, ask the user for theDEEPSEEK_API_KEY
first, then callset_provider({ provider: "deepseek", apiKey: "<key>" }).
The same shape works for openai, openrouter, local, codex,
and echo — see list_providers for the full catalog.
Recipe — "switch to agent X" / "be Athena now"
load_tools({ names: ["list_agents", "use_agent"] }), then calllist_agents({})and find the row matching the name or id.- Call
use_agent({ agentId: "<id or name>" }). The new agent's SOUL.md and provider override take effect on the
next turn.
Recipe — "set permissions to yolo" / "stop asking for approval"
load_tools({ names: ["set_approval_mode"] }), then callset_approval_mode({ mode: "yolo" })(or"auto"/"strict").Never shell out to
curl/the settings API for this — that bypasses
the registered tool and fails on auth.
Recipe — "what skills do you have"
load_tools({ names: ["list_skills"] }), then calllist_skills({})
(default returns all statuses). For "what skills can you use right
now", pass{ status: "enabled" }.Reply with names + brief descriptions. If the user asks for
detail on one, callread_skillwith that id.
Recipe — "disable browser tools"
load_tools({ names: ["list_toolsets", "disable_toolset"] }), then
calllist_toolsets({})to confirm thebrowsertoolset name.Call
disable_toolset({ toolset: "browser" }). The browser tools
stop being offered next turn; your self-config tools are unaffected.
Re-enable any time withenable_toolset({ toolset: "browser" }).
Recipe — "always auto-approve git commands"
load_tools({ names: ["get_self", "set_auto_approve_commands"] }),
then callget_self({})and readapprovalSettings.autoApproveCommands— the current allowlist.set_auto_approve_commandsREPLACES the list, so pass the existing
entries plus the new one:set_auto_approve_commands({ patterns: [...existing, "git "] }).
Dropping the existing entries here would silently un-approve them.
API and registered tools — not the CLI
Gini itself operates through /api/* and the registered tool catalog.
Shelling out to gini ... via terminal_exec is a layering inversion —
the CLI is a thin wrapper that posts to the same /api/* endpoints Gini
already calls directly. The agent should never use its own CLI to drive
its own runtime.
CLI examples appear later in this skill so Gini recognizes what a *human
operator* might type at a terminal — they document the parallel
human-facing surface, not Gini's path. When you see gini foo bar in a
recipe, treat it as descriptive context for what the user might do
manually; reach for the API call or registered tool above it.
Never read ~/.gini/instances/<inst>/*.json directly — hit /api/status
and friends. The API is the source of runtime truth.
Where State Lives
Runtime state belongs to /api/*; reach for the paths below only when a
user-facing answer requires naming the on-disk location (where sign-ins
persist, where a skill ends up).
~/.gini/instances/<inst>/chrome-profile/— Playwright Chromium
profile; persistent browser sign-ins land here.~/.gini/instances/<inst>/skills/<name>/— user-installed skills (the
agent's own writable skill dir); these land flat, no category subfolder.skills/<category>/<name>/(repo root) — built-in skills shipped with
Gini.~/.gini/instances/<inst>/workspace/— default workspace root forfile.*tools;file.writelands here unlessGINI_WORKSPACE
overrides it.
Browser
By DEFAULT the runtime drives a single per-instance headless Chrome it spawns
itself, against a per-instance profile at ~/.gini/instances/<inst>/chrome-profile/.
It launches lazily on the first browser tool call. Sign-ins land on disk and
survive runtime restarts. When a site needs a sign-in, the user signs in through
a live in-chat screencast of that same headless Chrome (browser_connect), so
the user and the agent act on one browser the whole time. As a power-user
option the user can instead attach the runtime to their OWN already-running
external Chrome over CDP (POST /api/browser/connect with {cdpUrl}); the
runtime drives that Chrome but never starts or stops the process. (The old
visible managed-window mode was removed — issue #420.)
Tool surface, grouped by role:
Navigation:
browser.navigate,browser.back,browser.tabs.{list,new,switch,close}.Interaction:
browser.click,browser.type,browser.press,browser.hover,browser.drag,browser.select_option,browser.scroll.Inspection:
browser.snapshot,browser.wait_for,browser.console,browser.vision.Side-effecting (approval-gated):
browser.upload_file. Plusbrowser.closeto tear down the session.
Interactive actions skip the approval gate because the snapshot itself
is the trace evidence; uploads are gated because they egress local
bytes.
Recipe — authenticated workflow on a new site
When a site needs a sign-in the user hasn't completed:
Navigate with
browser.navigate(headless). If the page is a sign-in /
OAuth / auth wall, call thebrowser_connecttool with the target URL —
do NOT report "sign-in needed" as a blocker.The user gets a Connect button in chat. Clicking it opens a live
screencast of the agent's headless Chrome at that page; they sign in once
and click "I've signed in". The cookies persist on disk.The agent continues against the now-signed-in profile — no relaunch, no
visible window. The sign-in survives later tasks and runtime restarts.
Human-operator CLI mirror (the same calls a person might run from a
terminal — not Gini's path): `gini browser {status | connect [--url WSURL] |
disconnect}. A bareconnect` is a no-op for the default spawned browser
(sign-in is the in-chat screencast, not a CLI flow); connect --url ws://...
attaches to the user's own external Chrome over CDP. To clear saved logins from
the spawned profile, rm -rf the per-instance profile dir.
Scheduled Jobs
Jobs run on an interval or a cron expression. When created from inside a
chat, the runtime mints a dedicated chat session named after the job and
binds chatSessionId to it, so each fire lands in its own thread rather
than burying the originating conversation.
Create an interval job:
POST /api/jobs
Content-Type: application/json
{
"name": "audible-renewal-check",
"intervalSeconds": 86400,
"prompt": "Open audible.com and confirm my subscription is still active."
}Create a cron job:
POST /api/jobs
{
"name": "morning-summary",
"cronExpression": "0 9 * * *",
"cronTimezone": "America/Los_Angeles",
"prompt": "Summarize new email and Slack pings since yesterday 9am."
}Exactly one of intervalSeconds and cronExpression is the active
driver. cronTimezone defaults to UTC.
Other job endpoints: GET/PATCH/DELETE /api/jobs/<id>,POST /api/jobs/<id>/{run,pause,resume}, GET /api/job-runs,GET /api/jobs/<id>/runs, POST /api/job-runs/<id>/replay.
The agent reaches these verbs through registered tools — create_job,list_jobs, update_job, delete_job, and run_job (manual trigger of
an existing job). Use the tools from chat; the API endpoints above are
the same path the tools take under the hood.
Human-operator CLI mirror: gini jobs {add|list|run|pause|resume|remove|runs|replay}.
Recipe — one-shot reminder
The user asks "remind me at 9am on 2026-08-19 to pause my Audible
subscription." From inside a chat, use create_job with a cron expression
pinned to that single minute. The chat-session binding happens
automatically. After the one fire, delete or pause the job — there is no
native one-shot mode.
Messaging — Telegram
The Telegram bridge speaks the Bot API over fetch and ingests messages
via long-polling getUpdates. No webhook URL is required. A local
instance behind NAT works the same as one on a public host.
Setup
Create a bot. Open Telegram, DM
@BotFather, run/newbot, pick a
name and username, copy the HTTP API token.Register the bridge with the bot token:
POST /api/messaging Content-Type: application/json { "name": "my-bot", "kind": "telegram", "deliveryTargets": [], "botToken": "<BOT_TOKEN>" }The response carries the bridge id and an initial status. The bot's
username isn't resolved yet — run the health probe next to learn the
actual handle.Human-operator CLI mirror:
gini messaging add my-bot telegram --bot-token <BOT_TOKEN>.Probe health to resolve the bot handle. This calls Telegram's
getMeand writesmetadata.botUsernameonto the bridge so later
prompts can say@<bot>instead of "your bot":POST /api/messaging/my-bot/healthA successful response reports
Connected as @<bot>.and the bridge
status flips toconfigured. Fix any token error before continuing.Human-operator CLI mirror:
gini messaging health my-bot.Enroll the user's chat. Have the user DM the bot anything
(including/start). The runtime mints a short verification code
(F971-8261format, 10-minute TTL), records it onbridge.metadata.recentDeniedChats[].verificationCodefor the
originating chat, and DMs the same code back to the user. Fetch the
pending list withGET /api/messaging/my-bot/chats, confirm theverificationCodematches what the user reports receiving, then
allow-list the chat in the next step. A DM after the code expires
mints a fresh one and replaces the row.Allow-list the chat ID so the bridge will deliver messages there:
POST /api/messaging/my-bot/allow { "chatId": 123456789, "expectedCode": "F971-8261" }Pass the
expectedCodeyou confirmed in step 4. The server re-checks
that it still matches the liveverificationCodeon the pending row
and hasn't expired, so a code that rotated (the user re-DM'd and
minted a new one) or aged past its TTL between fetch and approve
returns409 Conflictinstead of silently allow-listing the chat.Group chat IDs are negative integers — that is correct, not an
error. Group chats have noverificationCode(no per-user channel
to deliver one through), so omitexpectedCodewhen allow-listing a
negative chat ID.Human-operator CLI mirror:
gini messaging allow my-bot <chatId>.
The CLI omits the code because the explicit invocation on the
operator's machine already proves intent; the API path is the one
that needs the code-rotation check.Send a message to confirm round-trip:
POST /api/messaging/my-bot/send { "text": "Hello from Gini.", "target": "local" }Human-operator CLI mirror:
gini messaging send my-bot "Hello from Gini.".
Bridge kind supports telegram and demo today; future messengers
slot into the same /api/messaging shape.
The agent's tool for sending is send_message. messaging.send is
high-risk by classification, so it flows through the approval seam
exactly like file.write and terminal.exec — see the Approvals
section below for the three-mode contract (strict blocks, auto
auto-approves with a full audit trail, yolo skips the queue).
Inspecting state
API: GET /api/messaging, GET /api/messaging/<id>/{chats,messages},POST /api/messaging (create), POST /api/messaging/<id>/{health,disable,remove,allow,deny,reject-pending,send,receive}.
Human-operator CLI mirror: gini messaging {list|add|health|disable|remove|receive|send|messages|allow|deny|reject-pending|chats}. disable keeps the bridge row with status "disabled"; remove drops it. Telegram per-chat enrollment uses allow/deny/reject-pending/chats; Discord uses channel-as-auth via deliveryTargets (no per-chat allowlist).
MCP Servers
Register a local MCP server by command:
POST /api/mcp
Content-Type: application/json
{ "name": "fs-mcp", "command": "node", "args": ["/path/to/server.js"], "exposedTools": [] }Health probe and tool invocation:
POST /api/mcp/fs-mcp/health
POST /api/mcp/fs-mcp/invoke
{ "tool": "read_file", "args": { "path": "/tmp/x" } }Listing: GET /api/mcp.
exposedTools defaults to [], which exposes everything the server
advertises.
The agent's tool for calling registered MCP tools is mcp_call
(args: server, tool, arguments). It auto-executes — invocations
are NOT gated through the approval queue (the MCP server itself is
operator-registered so the agent can't reach arbitrary code). Each
call writes a mcp.tool.invoked audit row.
To enumerate the registered servers from chat, `load_tools({ names:
["listmcpservers"] })then calllistmcpservers({})`.
Human-operator CLI mirror:
gini mcp add fs-mcp node /path/to/server.js
gini mcp health fs-mcp
gini mcp invoke fs-mcp read_file '{"path":"/tmp/x"}'
gini mcp listConnectors
Connectors register external coding/issue services so subagents and
related skills can call them. Built-in providers: claude-code, codex,linear, demo, generic.
API:
GET /api/connectors/providers— discover what's installable.POST /api/connectors { provider, name, token }— register one.GET /api/connectors— list registered connectors.POST /api/connectors/<id>/health— health probe.PATCH /api/connectors/<id> { token }— rotate the credential.DELETE /api/connectors/<id>— remove.POST /api/connectors/detect— auto-detect locally installed CLIs.
From chat, load_tools({ names: ["list_connectors"] }) then calllist_connectors({}) for inventory, and request_connector to drive a
user-mediated add when one is missing.
Human-operator CLI mirror:
gini connectors providers
gini connectors add --provider claude-code --name claude-main --token <T>
gini connectors list
gini connectors health <id>
gini connectors remove <id>
gini connectors rotate <id> --token <T>
gini connectors detectSubagents (Delegated Coding)
Spawn a registered coder (Claude Code, Codex) to execute a delegated
prompt. From inside chat the agent should use the spawn_subagent tool;
the same call reaches the API path below.
API: POST /api/subagents { name, prompt }, GET /api/subagents.
Human-operator CLI mirror:
gini subagents spawn <connector-name> "Implement and commit the fix."
gini subagents listFor depth on prompting and tmux/PTY patterns, load skills/agents/claude-code/SKILL.md
or skills/agents/codex/SKILL.md — those skills cover --allowedTools,--max-turns, --full-auto, worktree layout, and dialog handling.
Memory
Three surfaces, no fourth:
USER.md(instance-scoped, always-inject) — user identity, preferences,
recurring goals. Edits go throughedit_user_profile, which
auto-approves: writes land at the approved file and ride the system
prompt on the next turn. Cross-agent — switching agents preserves
the user profile.SOUL.md(per-agent, always-inject) — agent persona and behavior
rules.edit_soulauto-approves a clean body (lands at the approved
file, rides the prompt next turn); the injection scanner routes a body
that trips a threat pattern throughSOUL.md.proposeduntil approved.Hindsight (per-agent SQLite bank, recall-on-demand) — long-term
memory populated by auto-retain at task end. Recall surfaces relevant
units automatically;recall_memoryis the on-demand lookup tool.
The legacy state.memories pinned-memory store, add_memory,update_memory, the /api/memory CRUD routes, andgini memory list|add|approve|reject were removed in the
memory-surface consolidation. The only API surfaces are the Hindsight
endpoints (/api/memory/retain, /api/memory/recall,/api/memory/reflect, /api/memory/units, /api/memory/banks) plus
the identity-file approve endpoint for SOUL.md
(POST /api/identity-files/soul/approve).
Human-operator CLI mirror: gini memory {retain|recall|reflect|units|banks|migrate}.
Skills
Built-in skills live under the repo at skills/<category>/<name>/SKILL.md.
User-installed skills land flat at~/.gini/instances/<inst>/skills/<name>/SKILL.md (no category subfolder).
The runtime loads both on boot.
To enumerate skills from chat, load_tools({ names: ["list_skills"] })
then call list_skills({}) (filter via { status, nameContains }). To
load a specific skill's body use read_skill.
For lifecycle operations the agent has three tools: install_skill
(lands a raw SKILL.md body), enable_skill, and disable_skill. Themeta/install-skill skill still drives the full install UX (parsing
pasted descriptions, drafting frontmatter); these tools are the fast
path when the SKILL.md text is already in hand.
API: GET /api/skills[/<id>], POST /api/skills,POST /api/skills/<id>/{enable,disable,test,rollback},PATCH /api/skills/<id>, GET /api/skills/validate.
Human-operator CLI mirror: gini skills {list|show|enable|disable|test|rollback|validate|search}.
To draft a new SKILL.md interactively, use meta/create-skill.
Approvals
The runtime classifies browser.upload_file (hard-coded) plus any tool
whose name contains write, exec, or send as high risk.file.write, terminal.exec, messaging.send, and similar all run
through the approval seam. Browser interactive actions
(browser.click, browser.type, browser.drag, browser.select_option,browser.tabs.{new,switch,close}) are medium and trace via snapshot
evidence — they do not block on approval. **The agent should proposehigh-risk actions and surface them to the queue — not refuse them.**
approvalMode lives on the runtime config and decides how the seam
treats each high-risk call: strict | auto | yolo. Set viaPATCH /api/settings/auto-approve.
strict — the side effect blocks until a human approves the row
viaPOST /api/authorizations/<id>/approve(or denies it).auto — the approval row is created with
status: "pending",
then the runtime auto-resolves it (status: "approved") and runs
the side effect without waiting for a human. The audit trail is
complete: the resolution audit row carriesevidence.autoApproved: trueplus `autoApprovedReason:
"approval-mode-auto"`.yolo — the approval row is still written and resolved
through the same auto-approve path, but the policy seam skips its
per-action gate check entirely. The resolution audit row carriesevidence.autoApproved: trueplus `autoApprovedReason:
"approval-mode-yolo"`, so the audit trail stays complete; only the
wait disappears. This is the install default; operators can switch
tostrictorautoviaPATCH /api/settings/auto-approve.
GET /api/authorizations
POST /api/authorizations/<id>/approve
POST /api/authorizations/<id>/denyHuman-operator CLI mirror:
gini approval list
gini approval approve <id>
gini approval deny <id>Troubleshooting
Telegram bridge stuck in error after health probe — re-probe viaPOST /api/messaging/<id>/health; the response is the updated bridge
record with its message field set. `Telegram bot token is missing —
recreate the bridge with a botToken.` means the token never landed;
recreate the bridge with the real token via POST /api/messaging
({ name, kind: "telegram", deliveryTargets: [], botToken: "<BOT_TOKEN>" }).
Any other message is the raw Telegram error from getMe(); the most
common is Unauthorized from a bad or revoked token — re-copy the token
from BotFather and recreate the bridge. (Human-operator CLI mirror:gini messaging health, thengini messaging add my-bot telegram --bot-token <BOT_TOKEN>.)
Spawned browser launch fails with "Failed to launch Chromium" — the
error names what to do (`Confirm Chrome / Chromium is installed (or set
GINICHROMEPATH), or run \bunx playwright install chromium\`). The runtime
auto-installs Playwright's Chromium when no browser is on disk; if that fails,
install Chrome or set GINI_CHROME_PATH to a Chromium-compatible executable
and retry the browser tool call. (This applies to the default spawned
transport; a cdp attach instead fails with a "Failed to attach over CDP"
message — disconnect and let the agent use its own spawned browser.)
Sign-in screencast won't open ("The agent's browser isn't running") —
the spawned Chrome wasn't live when Connect was clicked (e.g. after a gateway
restart). With a recorded page URL the runtime relaunches headless and
navigates there automatically; if it still can't, navigate to the page again
(browser.navigate) and re-call browser_connect.
User says a high-risk action is "stuck" — it is sitting in the
approval queue. Fetch GET /api/authorizations to see pending items, thenPOST /api/authorizations/<id>/approve or /deny. (Human-operator CLI mirror:gini approval list, then approve <id> or deny <id>.) The agent
should never refuse a high-risk action up front — propose it, let it
land in the queue, and wait for the user's decision.
Rules
Do not refuse a capability without first checking this skill and the
approval queue. Propose the action; let the user approve.*Gini operates through `/api/` and registered tools — never shells
out to its own CLI.** Callinggini ...viaterminal_execis a
layering inversion: the CLI is just a wrapper that posts to the same
endpoints. For state queries, runtime mutations, and capability
invocations, use the API directly (or the matching registered tool
when one exists, e.g.create_job,list_jobs,update_job,delete_job,run_job,spawn_subagent,read_skill, and the
self-config tools (load_toolsthem first:get_self,list_providers,set_provider,use_agent, …)).- Never read
~/.gini/instances/<inst>/*.jsondirectly — call/api/*. Persistent browser cookies are a feature. For sign-in, call
browser_connectonce (the in-chat screencast); do not ask the user to
re-authenticate on every run.When the user asks Gini to remember to do something later, create a
scheduled job — the runtime auto-binds it to a dedicated thread so
future fires don't bury the current conversation.For durable identity facts ("my name is X", "I prefer Y") call
edit_user_profileso they ride the prompt every turn across agents.
For ephemeral facts let auto-retain land them in Hindsight — never
narrate "I'll remember that" without actually calling a tool.