Compare commits

..

1 Commits

Author SHA1 Message Date
Noah Zweben MacBook
74b5ba85d7 Add flint plugin to marketplace 2026-03-24 16:38:27 -07:00
11 changed files with 24 additions and 107 deletions

View File

@@ -464,7 +464,7 @@
"source": {
"source": "url",
"url": "https://github.com/tryflint/claude-code-plugin.git",
"sha": "f3d56e33ed2fb3ed9b4f02e0fc65d0a79b24bf4d"
"sha": "8f57ef474f59817c7e5930dd6c295232d43c3711"
},
"homepage": "https://www.tryflint.com/docs/claude-code-plugin"
},

View File

@@ -76,7 +76,7 @@ Quick reference: IDs are **handle addresses** (`+15551234567` or `someone@icloud
| Tool | Purpose |
| --- | --- |
| `reply` | Send to a chat. `chat_id` + `text`, optional `files` (absolute paths). Auto-chunks text; files send as separate messages. |
| `chat_messages` | Fetch recent history as conversation threads. Each thread is labelled **DM** or **Group** with its participant list, then timestamped messages (oldest-first). Omit `chat_guid` to see every allowlisted chat at once, or pass one to drill in. Default 100 messages per chat. Reads `chat.db` directly — full native history. |
| `chat_messages` | Fetch recent history from a chat (oldest-first). Reads `chat.db` directly — full native history. Scoped to allowlisted chats. |
## What you don't get

View File

@@ -33,8 +33,7 @@ import { join, basename, sep } from 'path'
const STATIC = process.env.IMESSAGE_ACCESS_MODE === 'static'
const APPEND_SIGNATURE = process.env.IMESSAGE_APPEND_SIGNATURE !== 'false'
const SIGNATURE = '\nSent by Claude'
const CHAT_DB =
process.env.IMESSAGE_DB_PATH ?? join(homedir(), 'Library', 'Messages', 'chat.db')
const CHAT_DB = join(homedir(), 'Library', 'Messages', 'chat.db')
const STATE_DIR = process.env.IMESSAGE_STATE_DIR ?? join(homedir(), '.claude', 'channels', 'imessage')
const ACCESS_FILE = join(STATE_DIR, 'access.json')
@@ -142,21 +141,6 @@ const qChatsForHandle = db.query<{ guid: string }, [string]>(`
WHERE c.style = 45 AND LOWER(h.id) = ?
`)
// Participants of a chat (other than yourself). For DMs this is one handle;
// for groups it's everyone in chat_handle_join.
const qChatParticipants = db.query<{ id: string }, [string]>(`
SELECT DISTINCT h.id FROM handle h
JOIN chat_handle_join chj ON chj.handle_id = h.ROWID
JOIN chat c ON c.ROWID = chj.chat_id
WHERE c.guid = ?
`)
// Group-chat display name and style. display_name is NULL for DMs and
// unnamed groups; populated when the user has named the group in Messages.
const qChatInfo = db.query<{ display_name: string | null; style: number }, [string]>(`
SELECT display_name, style FROM chat WHERE guid = ?
`)
type AttRow = { filename: string | null; mime_type: string | null; transfer_name: string | null }
const qAttachments = db.query<AttRow, [number]>(`
SELECT a.filename, a.mime_type, a.transfer_name
@@ -492,43 +476,15 @@ function messageText(r: Row): string {
return r.text ?? parseAttributedBody(r.attributedBody) ?? ''
}
// Build a human-readable header for one conversation. Labels DM vs group and
// lists participants so the assistant can tell threads apart at a glance.
function conversationHeader(guid: string): string {
const info = qChatInfo.get(guid)
const participants = qChatParticipants.all(guid).map(p => p.id)
const who = participants.length > 0 ? participants.join(', ') : guid
if (info?.style === 43) {
const name = info.display_name ? `"${info.display_name}" ` : ''
return `=== Group ${name}(${who}) ===`
}
return `=== DM with ${who} ===`
}
// Render one chat's messages as a conversation block: header, then one line
// per message with a local-time stamp. A date line is inserted whenever the
// calendar day rolls over so long histories stay readable without repeating
// the full date on every row.
function renderConversation(guid: string, rows: Row[]): string {
const lines: string[] = [conversationHeader(guid)]
let lastDay = ''
for (const r of rows) {
const d = appleDate(r.date)
const day = d.toDateString()
if (day !== lastDay) {
lines.push(`-- ${day} --`)
lastDay = day
}
const hhmm = d.toTimeString().slice(0, 5)
const who = r.is_from_me ? 'me' : (r.handle_id ?? 'unknown')
const atts = r.cache_has_attachments ? ' [attachment]' : ''
// Tool results are newline-joined; a multi-line message would forge
// adjacent rows. chat_messages is allowlist-scoped, but a configured group
// can still have untrusted members.
const text = messageText(r).replace(/[\r\n]+/g, ' ⏎ ')
lines.push(`[${hhmm}] ${who}: ${text}${atts}`)
}
return lines.join('\n')
function renderMsg(r: Row): string {
const who = r.is_from_me ? 'me' : (r.handle_id ?? 'unknown')
const ts = appleDate(r.date).toISOString()
const atts = r.cache_has_attachments ? ' +att' : ''
// Tool results are newline-joined; a multi-line message would forge
// adjacent rows. chat_messages is allowlist-scoped, but a configured group
// can still have untrusted members.
const text = messageText(r).replace(/[\r\n]+/g, ' ⏎ ')
return `[${ts}] ${who}: ${text} (id: ${r.guid}${atts})`
}
// --- mcp ---------------------------------------------------------------------
@@ -628,19 +584,14 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
{
name: 'chat_messages',
description:
'Fetch recent iMessage history as readable conversation threads. Each thread is labelled DM or Group with its participant list, followed by timestamped messages. Omit chat_guid to see all allowlisted chats at once; pass a specific chat_guid to drill into one thread. Reads chat.db directly — full native history, scoped to allowlisted chats only.',
'Fetch recent messages from an iMessage chat. Reads chat.db directly — full native history. Scoped to allowlisted chats only.',
inputSchema: {
type: 'object',
properties: {
chat_guid: {
type: 'string',
description: 'A specific chat_id to read. Omit to read from every allowlisted chat.',
},
limit: {
type: 'number',
description: 'Max messages per chat (default 100, max 500).',
},
chat_guid: { type: 'string', description: 'The chat_id from the inbound message.' },
limit: { type: 'number', description: 'Max messages (default 20).' },
},
required: ['chat_guid'],
},
},
],
@@ -688,25 +639,13 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
return { content: [{ type: 'text', text: sent === 1 ? 'sent' : `sent ${sent} parts` }] }
}
case 'chat_messages': {
const guid = args.chat_guid as string | undefined
const limit = Math.min((args.limit as number) ?? 100, 500)
const allowed = allowedChatGuids()
const targets = guid == null ? [...allowed] : [guid]
if (guid != null && !allowed.has(guid)) {
const guid = args.chat_guid as string
const limit = (args.limit as number) ?? 20
if (!allowedChatGuids().has(guid)) {
throw new Error(`chat ${guid} is not allowlisted — add via /imessage:access`)
}
if (targets.length === 0) {
return { content: [{ type: 'text', text: '(no allowlisted chats — configure via /imessage:access)' }] }
}
const blocks: string[] = []
for (const g of targets) {
const rows = qHistory.all(g, limit).reverse()
if (rows.length === 0 && guid == null) continue
blocks.push(rows.length === 0
? `${conversationHeader(g)}\n(no messages)`
: renderConversation(g, rows))
}
const out = blocks.length === 0 ? '(no messages)' : blocks.join('\n\n')
const rows = qHistory.all(guid, limit).reverse()
const out = rows.length === 0 ? '(no messages)' : rows.map(renderMsg).join('\n')
return { content: [{ type: 'text', text: out }] }
}
default:

View File

@@ -1,7 +0,0 @@
{
"name": "terraform",
"description": "The Terraform MCP Server provides seamless integration with Terraform ecosystem, enabling advanced automation and interaction capabilities for Infrastructure as Code (IaC) development.",
"author": {
"name": "HashiCorp"
}
}

View File

@@ -1,12 +0,0 @@
{
"terraform": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e", "TFE_TOKEN=${TFE_TOKEN}",
"hashicorp/terraform-mcp-server:0.4.0"
]
}
}

View File

@@ -1,6 +1,5 @@
{
"name": "explanatory-output-style",
"version": "1.0.0",
"description": "Adds educational insights about implementation choices and codebase patterns (mimics the deprecated Explanatory output style)",
"author": {
"name": "Anthropic",

View File

@@ -6,7 +6,7 @@
"hooks": [
{
"type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.sh\""
"command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.sh"
}
]
}

View File

@@ -1,6 +1,5 @@
{
"name": "learning-output-style",
"version": "1.0.0",
"description": "Interactive learning mode that requests meaningful code contributions at decision points (mimics the unshipped Learning output style)",
"author": {
"name": "Anthropic",

View File

@@ -6,7 +6,7 @@
"hooks": [
{
"type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.sh\""
"command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.sh"
}
]
}

View File

@@ -1,6 +1,5 @@
{
"name": "ralph-loop",
"version": "1.0.0",
"description": "Continuous self-referential AI loops for interactive iterative development, implementing the Ralph Wiggum technique. Run Claude in a while-true loop with the same prompt until task completion.",
"author": {
"name": "Anthropic",

View File

@@ -6,7 +6,7 @@
"hooks": [
{
"type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh\""
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh"
}
]
}