Shows the SQL that the main session and its subagents run through the snowflake-raw MCP server in a pane, with each query's agent, status, and outcome.

<img src="assets/personal-logo.png" alt="Personal logo" width="160">
A collection of skills and custom agents for Claude Code, Codex, and Cursor CLI. Claude Code and Cursor install three domain plugins; Codex installs the shared skills directly from this checkout.
Skills ship in three plugins, one per domain. A skill is invoked as /<group>:<skill-name> — the prefix is the plugin it came from, and it is not optional.
craft — how code gets written and checked| Skill | Invoke | Description |
|---|---|---|
architecture-baseline | /craft:architecture-baseline | Decide a new project's architectural constraints before feature work. Requires the separately installed researcher agent. |
python-engineering-standards | /craft:python-engineering-standards | Apply production Python standards for layout, typing, configuration, logging, error handling, testing, and packaging. |
code-audit | /craft:code-audit | Audit code in any language and write a machine-readable report covering correctness, security, performance, architecture, error handling, and readability. |
structure-review | /craft:structure-review | Review the shape and project conformance of a finished change before merge or archive. |
flow — how work gets driven from idea to build| Skill | Invoke | Description |
|---|---|---|
scout | /flow:scout | Explore work whose shape is not settled and return a decision-ready briefing. Requires the separately installed pathfinder and researcher agents. |
grill-me | /flow:grill-me | Pressure-test ideas and change artifacts before implementation. |
orchestrate | /flow:orchestrate | Drive workers to implement a bounded plan. Requires the separately installed implementer, pathfinder, and researcher agents. |
write-ticket | /flow:write-ticket | Write Jira tickets and comments in plain language through the Atlassian MCP. |
stash | /flow:stash | Park raw content in an Obsidian vault inbox for later processing. |
data — the warehouse surface| Skill | Invoke | Description |
|---|---|---|
sql-data-analysis | /data:sql-data-analysis | Apply SQL standards for analytics, reporting, extraction, and transformation. |
data-governance | /data:data-governance | Query Snowflake's ACCOUNT_USAGE schema for masking, classification, access history, roles, and user auditing. |
Each group directory under skills/ is one Claude Code or Cursor plugin. A member's SKILL.md and any local scripts/, assets/, and references/ are installed together. Codex uses symlinks to these same source directories.
Installing a group installs all of its skills. Per-skill installation is not available; the Cursor fallback below is the only path that installs individual skills.
Use this repository URL:
https://github.com/victormacaubas/data-engineering-skills.git
In Claude Code, add the marketplace:
/plugin marketplace add https://github.com/victormacaubas/data-engineering-skills.git
Install the groups you want:
/plugin install craft@data-engineering-skills
/plugin install flow@data-engineering-skills
/plugin install data@data-engineering-skills
Skills then appear under / with their group prefix — /craft:structure-review, /flow:orchestrate, /data:sql-data-analysis. The prefix is applied by Claude Code and cannot be turned off.
Mods are Claude Code plugins that hook into a session and draw their own interface. Each mod installs on its own, independently of the skill groups:
| Mod | Description |
|---|---|
snowflake-query-panel | Shows the SQL that the main session and its subagents run through the snowflake-raw MCP server in a pane, grouped by agent. Requires the snowflake-raw MCP server. |
Install one the same way as a group:
/plugin install snowflake-query-panel@data-engineering-skills
Mods are available for Claude Code only. Cursor CLI and Codex have no equivalent, and their catalogs and installers do not list mods. The mod API is early access and may change between Claude Code releases. See docs/mods.md.
Open /plugin in Claude Code, refresh the data-engineering-skills marketplace, then update each installed group and mod plugin. Marketplace installs are snapshots rather than live symlinks to a checkout.
Clone this repository, then run the symlink-first installer:
git clone https://github.com/victormacaubas/data-engineering-skills.git
cd data-engineering-skills
./scripts/install-codex-skills.sh --skills all
Skills are linked into ~/.codex/skills/ by default and invoke unprefixed. Select individual skills or groups, or use copies where symlinks are unsuitable:
./scripts/install-codex-skills.sh --skills sql-data-analysis,data-governance
./scripts/install-codex-skills.sh --group data
CODEX_SKILLS_DIR=/custom/codex/skills ./scripts/install-codex-skills.sh --skills all
./scripts/install-codex-skills.sh --skills all --copy
Install the matching native TOML agents with the same installer used for Claude Code and Cursor CLI:
./scripts/install-agents.sh --platform codex --agents all
./scripts/install-agents.sh --platform codex --agents pathfinder,researcher
CODEX_AGENTS_DIR=/custom/codex/agents ./scripts/install-agents.sh --platform codex --agents all
Codex agent definitions live at agents/codex/*.toml and are linked into ~/.codex/agents/ by default.
Symlink installs reflect edits after git pull. If installed with --copy, rerun the same installer command after pulling. Rerun the installer after adding a new skill.
In an interactive Cursor CLI session:
/plugin marketplace add https://github.com/victormacaubas/data-engineering-skills.git
Or register the marketplace from a shell:
agent plugin marketplace add https://github.com/victormacaubas/data-engineering-skills.git
Then open /plugin in Cursor CLI to browse the registered marketplace and install the craft, flow, and data plugins. Public Cursor Marketplace submission is not required.
If Cursor CLI reports [permission_denied] Third-party plugin imports are disabled by team admin settings, ask an admin to import this repository as a Team Marketplace. When plain user skills are permitted, you can instead clone this repository and install skills directly:
./scripts/install-cursor-skills.sh --skills all
./scripts/install-cursor-skills.sh --skills architecture-baseline,sql-data-analysis
./scripts/install-cursor-skills.sh --group data
./scripts/install-cursor-skills.sh --group craft,data
--skills takes bare skill names and --group takes group names; the two are mutually exclusive. Either way the target directory is flat, so fallback skills are invoked unprefixed — structure-review, not craft:structure-review. That is the one behavioural difference from a marketplace install.
The fallback symlinks skills into ~/.cursor/skills/ by default. It preserves unselected skills and backs up conflicting non-repository paths. Use --copy for copies or override the target:
CURSOR_SKILLS_DIR=/custom/cursor/skills \
./scripts/install-cursor-skills.sh --skills all
This fallback installs plain Cursor skills, not plugins. Marketplace installation remains the preferred distribution path.
Open /plugin in Cursor CLI to refresh the marketplace and update installed groups. For fallback symlinks, git pull updates the linked skills; rerun install-cursor-skills.sh after pulling only when the fallback used --copy or when a new skill was added.
Marketplace plugins contain skills only. Install these agents separately before using an agent-dependent skill:
| Skill | Required agents |
|---|---|
/craft:architecture-baseline | researcher |
/flow:orchestrate | implementer, pathfinder, researcher |
/flow:scout | pathfinder, researcher |
For example:
./scripts/install-agents.sh --platform both --agents implementer,pathfinder,researcher
Clone the repository before running its agent installer:
git clone https://github.com/victormacaubas/data-engineering-skills.git
cd data-engineering-skills
Run the interactive agent-only wizard:
./scripts/install.sh
For non-interactive installs:
./scripts/install-agents.sh --platform claude --agents all
./scripts/install-agents.sh --platform codex --agents all
./scripts/install-agents.sh --platform cursor --agents pathfinder,researcher
./scripts/install-agents.sh --platform both --agents implementer,pathfinder,researcher
The default mode creates symlinks. Use --copy when links are unsuitable:
./scripts/install-agents.sh --platform both --agents all --copy
Claude Code agents install into ~/.claude/agents/; Codex agents install into ~/.codex/agents/; Cursor CLI agents install into ~/.cursor/agents/. Override any target with an environment variable:
CLAUDE_AGENTS_DIR=/custom/claude/agents \
./scripts/install-agents.sh --platform claude --agents all
CURSOR_AGENTS_DIR=/custom/cursor/agents \
./scripts/install-agents.sh --platform cursor --agents all
CODEX_AGENTS_DIR=/custom/codex/agents \
./scripts/install-agents.sh --platform codex --agents all
The installer backs up an existing non-repository target as <path>.bak.<timestamp>. It does not remove unselected agents.
Agent symlinks reflect edits after git pull. If agents were installed with --copy, rerun the same agent install command after pulling.
Use /plugin in Claude Code or Cursor CLI to uninstall and manage marketplace skills. For Codex, remove the confirmed skill path from ~/.codex/skills/.
For a Cursor fallback installation, remove the confirmed skill path from ~/.cursor/skills/.
To uninstall a custom agent, remove its file or symlink from the relevant target:
rm ~/.claude/agents/<agent-name>.md
rm ~/.cursor/agents/<agent-name>.md
Removing an agent does not uninstall any skill that depends on it.
Earlier releases installed skills directly under ~/.claude/skills/ and ~/.codex/skills/. No marketplace command or agent installer removes old files, symlinks, directories, or .bak.<timestamp> backups.
Install and verify the replacement first. Then inspect any legacy target before deleting it:
ls -ld ~/.claude/skills/<skill-name> ~/.codex/skills/<skill-name> 2>/dev/null
readlink ~/.claude/skills/<skill-name> 2>/dev/null || true
readlink ~/.codex/skills/<skill-name> 2>/dev/null || true
Remove only a path you have confirmed is an obsolete repository-owned link or an unneeded copy. Preserve customized copies and backups. If both a marketplace plugin and a legacy install remain, the client may discover the skill twice.
data-engineering-skills/
├── .claude-plugin/marketplace.json # Claude Code catalog: skill groups and mods
├── .cursor-plugin/marketplace.json # Cursor CLI skill catalog
├── skills/
│ └── <group>/ # craft | flow | data — one plugin each
│ ├── README.md
│ └── <skill-name>/
│ ├── SKILL.md
│ ├── scripts/
│ ├── assets/
│ └── references/
├── mods/
│ └── <mod-name>/ # Claude Code only, one plugin each
│ ├── .claude-plugin/plugin.json
│ ├── hooks/
│ └── types/
├── agents/
│ ├── claude/<agent-name>.md
│ ├── codex/<agent-name>.toml
│ └── cursor/<agent-name>.md
├── scripts/ # Agent installers and Cursor/Codex skill installers
├── docs/ # Authoring guides
└── openspec/ # Tracked changes
See docs/authoring.md to create or graduate a skill, docs/mods.md to author a Claude Code mod, and docs/agents.md to author platform-specific agents.
A marketplace is not listed
Check that the Git URL is reachable, then add it again with the marketplace-add command for that client. A recent client version with Git-backed marketplace support is required. If Cursor team policy blocks third-party imports, use the documented Team Marketplace or plain-skill fallback.
A skill is missing from a marketplace
A skill loads only when its group entry lists it. Check that the skill directory sits under skills/<group>/ and that its path appears in that group's skills array in both catalogs — a skill present on disk but absent from the array loads nowhere, silently. Work under skills/in-progress/ and skills/deprecated/ is excluded by design.
A skill appears twice
Most often an old per-skill plugin is still installed alongside its group; the two show as /structure-review:structure-review and /craft:structure-review. Uninstall the old one. Otherwise inspect ~/.claude/skills/<skill-name> and, for legacy cleanup only, ~/.codex/skills/<skill-name>, and remove only the obsolete installation after verifying whether it is a symlink, copy, or customized directory.
An agent runs without its skill
Agent skill preloads are namespaced (craft:structure-review). A preload that does not resolve is skipped silently, with only a debug-log warning, so the agent still launches and still returns output. If a review agent's output looks unlike the skill's method, check its skills: frontmatter against the current group names.
An agent-dependent skill cannot dispatch a worker
Install the required agent names for the current platform with scripts/install-agents.sh, then restart or reload the client if needed.
An installed agent does not appear
Check ~/.claude/agents/, ~/.codex/agents/, or ~/.cursor/agents/ and rerun the installer for that platform.
hooks/register.tsx 193 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { QueryRow } from '../types'
5import { fallbackAgentLabel, formatCount, groupByAgent, oneLine, rowParts, summaryLine, truncate } from './format'
6import { firstLine, outcomeOf } from './outcome'
7import type { Outcome } from './outcome'
8
9const PLUGIN = 'snowflake-query-panel'
10const SQL_TOOL = 'mcp__snowflake-raw__sql_exec_tool'
11const PANE_ID = 'snowflake'
12const PANE_TITLE = 'Snowflake'
13const PANE_ROWS = 6
14const COMMAND = 'sql-panel'
15const MAIN = 'main'
16const MAX_ROWS = 10
17const DEFAULT_COLUMNS = 100
18const ROW_INDENT = 2
19
20const rows = atom({ plugin: 'snowflake-query-panel', key: 'rows' } as const, [])
21const agentLabels = atom({ plugin: 'snowflake-query-panel', key: 'agentLabels' } as const, {})
22const expanded = atom({ plugin: 'snowflake-query-panel', key: 'expanded' } as const, [])
23const toastShown = atom({ plugin: 'snowflake-query-panel', key: 'toastShown' } as const, false)
24
25const isRecord = (value: unknown): value is Record<string, unknown> =>
26 typeof value === 'object' && value !== null
27
28const sqlOf = (e: Record<string, unknown>): string => {
29 if (typeof e.sql === 'string') {
30 return e.sql
31 }
32
33 return isRecord(e.input) && typeof e.input.sql === 'string' ? e.input.sql : ''
34}
35
36async function quietly<T>($: EngineInterface, work: () => Promise<T>): Promise<T | undefined> {
37 try {
38 return await work()
39 } catch (error) {
40 const reason = error instanceof Error ? error.message : String(error)
41 $.ui.log(`${PLUGIN}: recording failed: ${reason}`, { to: 'debug' })
42
43 return undefined
44 }
45}
46
47async function cacheAgentLabel($: EngineInterface, agentKey: string): Promise<void> {
48 if ((await read($, agentLabels))[agentKey] !== undefined) {
49 return
50 }
51 const listed = agentKey === MAIN ? undefined : (await $.agent.list()).find(a => a.id === agentKey)
52 const label =
53 agentKey === MAIN
54 ? MAIN
55 : listed === undefined
56 ? fallbackAgentLabel(agentKey)
57 : `${listed.type} · ${listed.name || listed.description}`
58 await update($, agentLabels, labels => ({ ...labels, [agentKey]: label }))
59}
60
61const openPane = ($: EngineInterface) =>
62 $.ui.open({ id: PANE_ID, title: PANE_TITLE, rows: PANE_ROWS })
63
64async function openOnFirstQuery($: EngineInterface): Promise<void> {
65 const opened = await openPane($)
66 if (opened.isPlaced || (await read($, toastShown))) {
67 return
68 }
69 await update($, toastShown, () => true)
70 $.ui.toast(`Snowflake panel waiting: widen the terminal or run /${COMMAND}`)
71}
72
73async function addRunningRow($: EngineInterface, row: QueryRow): Promise<void> {
74 await quietly($, () => cacheAgentLabel($, row.agentKey))
75 const held = await update($, rows, list => [...list, row].slice(-MAX_ROWS))
76 if (held.length === 1) {
77 await openOnFirstQuery($)
78 }
79}
80
81const finishRow = ($: EngineInterface, id: string, outcome: Outcome, durationMs?: number) =>
82 update($, rows, list =>
83 list.map((row): QueryRow => {
84 if (row.id !== id) {
85 return row
86 }
87
88 return outcome.status === 'failed'
89 ? { ...row, status: 'failed', durationMs, error: outcome.error }
90 : { ...row, status: 'succeeded', durationMs, queryId: outcome.queryId, rowCount: outcome.rowCount }
91 }),
92 )
93
94const toggle = (ids: string[], id: string): string[] =>
95 ids.includes(id) ? ids.filter(one => one !== id) : [...ids, id]
96
97export const register: Register = on => {
98 on('session.start', async ($, e, next) => {
99 await quietly($, () =>
100 $.command.register({
101 name: COMMAND,
102 description: 'Open the Snowflake query panel',
103 }),
104 )
105
106 return next(e)
107 })
108
109 on('command.run', { command: COMMAND }, async $ => {
110 await openPane($)
111
112 return {}
113 })
114
115 on('tool.call', { tool: SQL_TOOL }, async ($, e, next) => {
116 const id = e.tool_use_id
117 const row: QueryRow = { id, agentKey: e.agentId ?? MAIN, sql: sqlOf(e), status: 'running' }
118 await quietly($, () => addRunningRow($, row))
119
120 const startedAt = await quietly($, () => $.clock.now())
121 let outcome: Outcome = { status: 'failed', error: 'Query did not complete' }
122 try {
123 const ran = await next(e)
124 outcome = outcomeOf(ran)
125
126 return ran
127 } catch (error) {
128 outcome = { status: 'failed', error: firstLine(error instanceof Error ? error.message : String(error)) }
129 throw error
130 } finally {
131 await quietly($, async () => {
132 const endedAt = await $.clock.now()
133 const durationMs = startedAt === undefined ? undefined : endedAt - startedAt
134 await finishRow($, id, outcome, durationMs)
135 })
136 }
137 }).catch(($, e, next) => next(e))
138
139 on('ui.render', { component: 'Pane', requestId: PANE_ID }, async ($, e) => {
140 const { Box, Button, Code, Text } = $.ui.resolve(e)
141 const list = await read($, rows)
142 const labels = await read($, agentLabels)
143 const openIds = await read($, expanded)
144 const width = (e.props.bodyColumns || e.viewport?.columns) ?? DEFAULT_COLUMNS
145
146 return (
147 <Box flexDirection="column">
148 <Text dimColor>{truncate(summaryLine(list), width)}</Text>
149 {groupByAgent(list).map((group, index) => (
150 <Box flexDirection="column" marginTop={index > 0 ? 1 : 0}>
151 <Text bold>
152 {labels[group.agentKey] ?? fallbackAgentLabel(group.agentKey)} · {formatCount(group.rows.length, 'query', 'queries')}
153 </Text>
154 {group.rows.map(row => {
155 const isOpen = openIds.includes(row.id)
156 const { label, status } = rowParts(row, width - ROW_INDENT, isOpen)
157
158 return (
159 <Box flexDirection="column" paddingLeft={ROW_INDENT}>
160 <Box flexDirection="row" justifyContent="space-between">
161 <Button
162 key={`row:${row.id}`}
163 plain
164 label={label}
165 onPress={() => update($, expanded, ids => toggle(ids, row.id))}
166 />
167 <Box flexDirection="row">
168 {status.map(piece => (
169 <Text {...piece.style}>{piece.text}</Text>
170 ))}
171 </Box>
172 </Box>
173 {row.status === 'failed' && (
174 <Text color="error" wrap="wrap">
175 {isOpen ? row.error : truncate(oneLine(row.error ?? ''), width - ROW_INDENT)}
176 </Text>
177 )}
178 {isOpen && (
179 <Box flexDirection="column" borderStyle="round" borderColor="subtle">
180 <Code source={row.sql} language="sql" wrap="wrap" />
181 {row.queryId !== undefined && <Text dimColor>query id {row.queryId}</Text>}
182 </Box>
183 )}
184 </Box>
185 )
186 })}
187 </Box>
188 ))}
189 </Box>
190 )
191 })
192}
193hooks/format.ts 96 lines1import type { QueryRow } from '../types'
2
3export type AgentGroup = { agentKey: string; rows: QueryRow[] }
4
5const MIN_SQL_CHARS = 12
6
7const SHORT_AGENT_ID_CHARS = 6
8
9export const fallbackAgentLabel = (agentKey: string): string =>
10 `agent ${agentKey.slice(0, SHORT_AGENT_ID_CHARS)}`
11
12export const oneLine = (sql: string): string => sql.replace(/\s+/g, ' ').trim()
13
14export const truncate = (text: string, max: number): string =>
15 text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text
16
17export const formatDuration = (ms: number): string =>
18 ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`
19
20export const formatCount = (count: number, singular: string, plural = `${singular}s`): string =>
21 `${count} ${count === 1 ? singular : plural}`
22
23export function outcomeParts(row: QueryRow): string[] {
24 if (row.status === 'running') {
25 return ['running']
26 }
27
28 return [
29 row.durationMs === undefined ? '' : formatDuration(row.durationMs),
30 row.rowCount === undefined ? '' : formatCount(row.rowCount, 'row'),
31 ].filter(part => part !== '')
32}
33
34const GLYPHS = { running: '◌', succeeded: '✓', failed: '✗' } as const
35const GLYPH_COLORS = { running: 'warning', succeeded: 'success', failed: 'error' } as const
36const SEPARATOR = ' · '
37const ARROW_WIDTH = 2
38const GAP_WIDTH = 1
39
40export type StatusStyle = { color?: 'success' | 'error' | 'warning'; dimColor?: true; bold?: true }
41export type StatusPiece = { text: string; style: StatusStyle }
42
43export function statusPieces(row: QueryRow): StatusPiece[] {
44 const glyph: StatusPiece = {
45 text: GLYPHS[row.status],
46 style: row.status === 'failed' ? { color: GLYPH_COLORS.failed, bold: true } : { color: GLYPH_COLORS[row.status] },
47 }
48 const detail = outcomeParts(row).join(SEPARATOR)
49
50 return detail === '' ? [glyph] : [glyph, { text: ` ${detail}`, style: { dimColor: true } }]
51}
52
53export const statusWidth = (pieces: StatusPiece[]): number =>
54 pieces.reduce((total, piece) => total + piece.text.length, 0)
55
56export function sqlLabel(sql: string, room: number, isOpen = false): string {
57 if (room < 1) {
58 return ''
59 }
60
61 return truncate(`${isOpen ? '▾' : '▸'} ${oneLine(sql)}`, room)
62}
63
64export function rowParts(row: QueryRow, width: number, isOpen = false): { label: string; status: StatusPiece[] } {
65 const full = statusPieces(row)
66 const fits = width - statusWidth(full) - GAP_WIDTH - ARROW_WIDTH >= MIN_SQL_CHARS
67 const status = fits ? full : full.slice(0, 1)
68
69 return { label: sqlLabel(row.sql, width - statusWidth(status) - GAP_WIDTH, isOpen), status }
70}
71
72export function summaryLine(rows: QueryRow[]): string {
73 const last = rows[rows.length - 1]
74 if (last === undefined) {
75 return 'Snowflake · no queries yet'
76 }
77 const total = `Snowflake · ${formatCount(rows.length, 'query', 'queries')}`
78 const running = rows.filter(row => row.status === 'running').length
79 if (running > 0) {
80 return `${total} · ${running} running`
81 }
82 const outcome =
83 last.status === 'failed' ? 'failed' : ['ok', ...outcomeParts(last)].join(' ')
84
85 return `${total} · last: ${outcome}`
86}
87
88export function groupByAgent(rows: QueryRow[]): AgentGroup[] {
89 const groups = new Map<string, QueryRow[]>()
90 for (const row of [...rows].reverse()) {
91 groups.set(row.agentKey, [...(groups.get(row.agentKey) ?? []), row])
92 }
93
94 return [...groups].map(([agentKey, grouped]) => ({ agentKey, rows: grouped }))
95}
96hooks/outcome.ts 76 lines1import type { ToolCallResult } from 'claude-code'
2
3export type Outcome =
4 | { status: 'succeeded'; queryId?: string; rowCount?: number }
5 | { status: 'failed'; error: string }
6
7const ERROR_MAX_CHARS = 200
8
9const isRecord = (value: unknown): value is Record<string, unknown> =>
10 typeof value === 'object' && value !== null
11
12export function firstLine(text: string, max = ERROR_MAX_CHARS): string {
13 const line = text.trim().split('\n')[0] ?? ''
14
15 return line.length > max ? `${line.slice(0, max - 1)}…` : line
16}
17
18function textOf(value: unknown): string | undefined {
19 if (typeof value === 'string') {
20 return value
21 }
22 if (isRecord(value) && Array.isArray(value.content)) {
23 const texts = value.content.flatMap((block: unknown) =>
24 isRecord(block) && block.type === 'text' && typeof block.text === 'string'
25 ? [block.text]
26 : [],
27 )
28
29 return texts.length > 0 ? texts.join('\n') : undefined
30 }
31
32 return undefined
33}
34
35function parseJson(text: string): unknown {
36 try {
37 return JSON.parse(text)
38 } catch {
39 return undefined
40 }
41}
42
43function payloadOf(ran: ToolCallResult): unknown {
44 const text = ran.text ?? textOf(ran.result)
45
46 return text === undefined ? ran.result : parseJson(text)
47}
48
49function succeeded(payload: unknown): Outcome {
50 if (!isRecord(payload)) {
51 return { status: 'succeeded' }
52 }
53 const meta = isRecord(payload.result_set) ? payload.result_set.resultSetMetaData : undefined
54 const numRows = isRecord(meta) ? meta.numRows : undefined
55
56 return {
57 status: 'succeeded',
58 ...(typeof payload.query_id === 'string' && { queryId: payload.query_id }),
59 ...(typeof numRows === 'number' && { rowCount: numRows }),
60 }
61}
62
63export function outcomeOf(ran: ToolCallResult): Outcome {
64 if (ran.deny !== undefined) {
65 return { status: 'failed', error: firstLine(ran.deny) }
66 }
67 const isError = ran.isError === true || (isRecord(ran.result) && ran.result.isError === true)
68 if (isError) {
69 const text = ran.text ?? textOf(ran.result) ?? ''
70
71 return { status: 'failed', error: firstLine(text) || 'Query failed' }
72 }
73
74 return succeeded(payloadOf(ran))
75}
76types/index.d.ts 26 lines1export type QueryStatus = 'running' | 'succeeded' | 'failed'
2
3export type QueryRow = {
4 /** The tool_use_id of the call; the key every update finds its row by. */
5 id: string
6 /** The calling loop's agent id, or `main` for the main session. */
7 agentKey: string
8 sql: string
9 status: QueryStatus
10 durationMs?: number
11 queryId?: string
12 rowCount?: number
13 error?: string
14}
15
16declare module 'claude-code' {
17 interface PluginState {
18 'snowflake-query-panel': {
19 rows: QueryRow[]
20 agentLabels: Record<string, string>
21 expanded: string[]
22 toastShown: boolean
23 }
24 }
25}
26