Documentation
¶
Index ¶
- Constants
- Variables
- func BindingFor(p Provider, configDir string) config.Binding
- func DetectInstalled() []string
- func MaterializeOpenCodeSession(ctx context.Context, source ocSource, sessionID, outputPath string, ...) (int, error)
- func NormalizeName(name string) (string, error)
- func OpenCodeDBPath() (string, error)
- func OpencodeChildBackendName(childSessionID string) string
- func OrderedNames() []string
- func ResetStateDBPathCacheForTest()
- func ResetWalkUpRetryForTest()
- func SetWalkUpRetryForTest(attempts int, backoff time.Duration)
- func TruncateUTF8(s string, maxBytes int) string
- type AnnotationResult
- type ChunkView
- type ClaudeCode
- func (p ClaudeCode) AnnotateChunk(c ChunkView, _ bool, redact func(string) string) AnnotationResult
- func (ClaudeCode) CLIBinaryName() string
- func (ClaudeCode) ConfigDirFromTranscript(transcriptPath string) (string, error)
- func (p ClaudeCode) DefaultCWD(transcriptPath string) string
- func (ClaudeCode) DiscoverDescendants(DescendantRegistrar, string) error
- func (ClaudeCode) DiscoverWorkflowFiles(reg WorkflowRegistrar, allow func(fileType string) bool) (int, error)
- func (ClaudeCode) ExtractAgentIDsFromMessage(message map[string]interface{}) []string
- func (p ClaudeCode) ExtractMetadata(lines []string) SessionMetadata
- func (p ClaudeCode) FindParentPID() int
- func (p ClaudeCode) FindSessionByID(partialID string) (string, string, error)
- func (ClaudeCode) InitTranscript(TranscriptRegistrar, string, string) error
- func (p ClaudeCode) InstallHooks() (string, error)
- func (p ClaudeCode) InstallSkills() error
- func (p ClaudeCode) IsHooksInstalled() (bool, error)
- func (p ClaudeCode) IsProcess(pid int) bool
- func (p ClaudeCode) IsSkillInstalled(name string) bool
- func (ClaudeCode) MatchesProcess(cmd string) bool
- func (ClaudeCode) Name() string
- func (ClaudeCode) OnAlreadyRunning(string)
- func (p ClaudeCode) ParseSessionHook(r io.Reader) (HookInput, error)
- func (p ClaudeCode) ProjectsDir() (string, error)
- func (ClaudeCode) ReadHookInput(r io.Reader) (*types.ClaudeHookInput, error)
- func (p ClaudeCode) ReadSessionHookInput(r io.Reader) (*types.ClaudeHookInput, error)
- func (p ClaudeCode) ScanSessions() ([]SessionInfo, error)
- func (p ClaudeCode) SettingsPath() (string, error)
- func (ClaudeCode) ShouldSpawnForInput(HookInput) bool
- func (p ClaudeCode) StateDir() (string, error)
- func (ClaudeCode) SupportsCommitLinking() bool
- func (p ClaudeCode) UninstallHooks() (string, error)
- func (p ClaudeCode) UninstallSkills() error
- func (p ClaudeCode) ValidateTranscriptPath(path string) error
- func (ClaudeCode) WalkUpToRoot(sessionID string) (string, string, error)
- func (ClaudeCode) WriteHookResponse(w io.Writer, suppressOutput bool, systemMessage string) error
- type Codex
- func (p Codex) AnnotateChunk(c ChunkView, sentFirstUserMessage bool, redact func(string) string) AnnotationResult
- func (Codex) CLIBinaryName() string
- func (p Codex) ConfigPath() (string, error)
- func (p Codex) DefaultCWD(transcriptPath string) string
- func (p Codex) DiscoverDescendants(reg DescendantRegistrar, rootThreadUUID string) error
- func (Codex) DiscoverWorkflowFiles(WorkflowRegistrar, func(fileType string) bool) (int, error)
- func (Codex) ExtractFirstUserMessageFromLines(lines []string) string
- func (p Codex) ExtractMetadata(lines []string) SessionMetadata
- func (p Codex) FindParentPID() int
- func (p Codex) FindSessionByID(partialID string) (string, string, error)
- func (p Codex) InitTranscript(target TranscriptRegistrar, transcriptPath, externalID string) error
- func (p Codex) InstallHooks() (string, error)
- func (p Codex) InstallSkills() error
- func (p Codex) IsHooksInstalled() (bool, error)
- func (p Codex) IsProcess(pid int) bool
- func (p Codex) IsSkillInstalled(name string) bool
- func (p Codex) ListSubtree(rootThreadUUID string) ([]CodexThreadRow, error)
- func (Codex) MatchesProcess(cmd string) bool
- func (Codex) Name() string
- func (Codex) OnAlreadyRunning(string)
- func (p Codex) ParseSessionHook(r io.Reader) (HookInput, error)
- func (p Codex) ReadHookInput(r io.Reader) (*types.CodexHookInput, error)
- func (p Codex) ReadSessionHookInput(r io.Reader) (*types.CodexHookInput, error)
- func (p Codex) ReadSessionInfo(path string) (CodexSessionInfo, error)
- func (p Codex) ScanCodexSessions() ([]CodexSessionInfo, error)
- func (p Codex) ScanSessions() ([]SessionInfo, error)
- func (Codex) SessionIDFromRolloutPath(path string) (string, bool)
- func (p Codex) SessionsDir() (string, error)
- func (p Codex) ShouldSpawnForInput(in HookInput) bool
- func (p Codex) StateDBPath() (string, error)
- func (Codex) StateDir() (string, error)
- func (Codex) SupportsCommitLinking() bool
- func (p Codex) UninstallHooks() (string, error)
- func (p Codex) UninstallSkills() error
- func (p Codex) ValidateRolloutPath(path string) error
- func (p Codex) WalkUpToRoot(threadUUID string) (rootUUID, rootRolloutPath string, err error)
- func (Codex) WriteHookResponse(w io.Writer, suppressOutput bool, systemMessage string) error
- type CodexRolloutMetadata
- type CodexSessionInfo
- type CodexThreadRow
- type Cursor
- func (p Cursor) AnnotateChunk(c ChunkView, _ bool, redact func(string) string) AnnotationResult
- func (Cursor) CLIBinaryName() string
- func (Cursor) DefaultCWD(transcriptPath string) string
- func (Cursor) DiscoverDescendants(reg DescendantRegistrar, _ string) error
- func (Cursor) DiscoverWorkflowFiles(WorkflowRegistrar, func(string) bool) (int, error)
- func (Cursor) ExtractMetadata(lines []string) SessionMetadata
- func (p Cursor) FindParentPID() int
- func (p Cursor) FindSessionByID(partialID string) (string, string, error)
- func (Cursor) InitTranscript(TranscriptRegistrar, string, string) error
- func (p Cursor) InstallHooks() (string, error)
- func (p Cursor) InstallSkills() error
- func (p Cursor) IsHooksInstalled() (bool, error)
- func (p Cursor) IsProcess(pid int) bool
- func (p Cursor) IsSkillInstalled(name string) bool
- func (Cursor) MatchesProcess(cmd string) bool
- func (Cursor) Name() string
- func (Cursor) OnAlreadyRunning(string)
- func (p Cursor) ParseSessionHook(r io.Reader) (HookInput, error)
- func (p Cursor) ProjectsDir() (string, error)
- func (Cursor) ReadHookInput(r io.Reader) (*types.CursorHookInput, error)
- func (p Cursor) ReadSessionHookInput(r io.Reader) (*types.CursorHookInput, error)
- func (p Cursor) ScanSessions() ([]SessionInfo, error)
- func (Cursor) ShouldSpawnForInput(HookInput) bool
- func (Cursor) StateDir() (string, error)
- func (Cursor) SupportsCommitLinking() bool
- func (p Cursor) UninstallHooks() (string, error)
- func (p Cursor) UninstallSkills() error
- func (p Cursor) ValidateTranscriptPath(path string) error
- func (Cursor) WalkUpToRoot(sessionID string) (string, string, error)
- func (Cursor) WriteHookResponse(w io.Writer, _ bool, _ string) error
- type DescendantRegistrar
- type HookInput
- type OpenCodeCollector
- type OpenCodeDBReader
- func (r *OpenCodeDBReader) FirstUserMessageText(ctx context.Context, sessionID string) (string, error)
- func (r *OpenCodeDBReader) ListDescendants(ctx context.Context, rootSessionID string) ([]string, error)
- func (r *OpenCodeDBReader) ListRootSessions(ctx context.Context) ([]OpenCodeRootSession, error)
- func (r *OpenCodeDBReader) MatchSessionIDs(ctx context.Context, partialID string) ([]string, error)
- func (r *OpenCodeDBReader) ReadSession(ctx context.Context, sessionID, sinceMessageID string) ([]ocRawEnvelope, error)
- func (r *OpenCodeDBReader) ReadSessionInfo(ctx context.Context, sessionID string) (directory, parentID string, err error)
- func (r *OpenCodeDBReader) ResolveOpencodeRoot(ctx context.Context, sessionID string) (string, error)
- type OpenCodeRootSession
- type Opencode
- func (Opencode) AnnotateChunk(c ChunkView, sentFirstUserMessage bool, redact func(string) string) AnnotationResult
- func (Opencode) CLIBinaryName() string
- func (Opencode) DefaultCWD(transcriptPath string) string
- func (Opencode) DiscoverDescendants(reg DescendantRegistrar, externalID string) error
- func (Opencode) DiscoverWorkflowFiles(WorkflowRegistrar, func(string) bool) (int, error)
- func (Opencode) ExtractMetadata([]string) SessionMetadata
- func (Opencode) FindParentPID() int
- func (Opencode) FindSessionByID(partialID string) (string, string, error)
- func (Opencode) InitTranscript(TranscriptRegistrar, string, string) error
- func (p Opencode) InstallHooks() (string, error)
- func (p Opencode) InstallSkills() error
- func (p Opencode) IsHooksInstalled() (bool, error)
- func (Opencode) IsProcess(pid int) bool
- func (p Opencode) IsSkillInstalled(name string) bool
- func (Opencode) Name() string
- func (Opencode) OnAlreadyRunning(externalID string)
- func (p Opencode) ParseSessionHook(r io.Reader) (HookInput, error)
- func (p Opencode) PluginDir() (string, error)
- func (p Opencode) ReadSessionHookInput(r io.Reader) (*types.OpenCodeHookInput, error)
- func (Opencode) ScanSessions() ([]SessionInfo, error)
- func (Opencode) ShouldSpawnForInput(in HookInput) bool
- func (p Opencode) StateDir() (string, error)
- func (Opencode) SupportsCommitLinking() bool
- func (p Opencode) UninstallHooks() (string, error)
- func (p Opencode) UninstallSkills() error
- func (Opencode) WalkUpToRoot(sessionID string) (string, string, error)
- func (Opencode) WriteHookResponse(w io.Writer, _ bool, _ string) error
- type OpencodeDescendantRegistrar
- type Provider
- type RootTranscriptProvider
- type SessionInfo
- type SessionMetadata
- type SummaryLink
- type TranscriptRegistrar
- type WorkflowRegistrar
Constants ¶
const ( NameClaudeCode = "claude-code" NameCodex = "codex" NameOpencode = "opencode" NameCursor = "cursor" )
const ( FileTypeTranscript = "transcript" FileTypeAgent = "agent" FileTypeWorkflowJournal = "workflow_journal" )
FileType* are the sync file_type values the backend recognizes for a tracked file. FileTypeTranscript is a session's primary transcript/rollout; FileTypeAgent is any subagent sidechain (Claude agent-*.jsonl, Codex descendant rollouts, OpenCode child sessions, workflow subagent transcripts).
FileTypeWorkflowJournal is the sync file_type for a Claude workflow run's journal.jsonl (CF-533). The backend (CF-532) accepts and stores it but never Claude-parses it; it is excluded from token/transcript analytics. Workflow subagent transcripts use the ordinary FileTypeAgent file_type.
const ClaudeStateDirEnv = "CONFAB_CLAUDE_DIR"
ClaudeStateDirEnv is the environment variable to override the default Claude state directory.
const CodexStateDBEnv = "CONFAB_CODEX_STATE_DB"
CodexStateDBEnv overrides automatic state DB discovery. When set, points directly at a state_N.sqlite file (or any SQLite file with the expected schema). Used by tests; can also be set by power users debugging Codex state inconsistencies.
const CodexStateDirEnv = "CONFAB_CODEX_DIR"
const CursorStateDirEnv = "CONFAB_CURSOR_DIR"
CursorStateDirEnv overrides the default Cursor state directory (~/.cursor) for tests and non-standard installs.
const OpenCodeDBEnv = "CONFAB_OPENCODE_DB"
OpenCodeDBEnv overrides automatic OpenCode SQLite-DB discovery. When set, points directly at an opencode.db file (or any SQLite file with the expected schema). Used by tests; can also be set by power users debugging OpenCode session sync.
Variables ¶
var ErrNoProvider = errors.New("no provider specified")
ErrNoProvider is returned by Get/NormalizeName when the provider name is empty. The empty name used to silently alias to claude-code; that fallback was a correctness hazard now that multiple providers exist (kata frm7), so callers must pass a concrete provider name (or route through DetectInstalled).
var LookPath = exec.LookPath
LookPath is the package-level seam tests stub to simulate CLI presence.
var StateDirPresent = stateDirPresent
StateDirPresent is the package-level seam tests stub to simulate a provider's state/config dir being present. It reports whether the dir into which the provider installs its hooks/plugins exists — the "configured locally" signal (desktop-app or CLI) that complements binary-on-PATH. Defaults to stateDirPresent (inspects the filesystem).
Functions ¶
func BindingFor ¶ added in v0.17.1
BindingFor resolves the backend binding for p's (provider, configDir). It is the single place that pairs a provider with config-dir binding resolution (kata hpec): it fills in the provider's DEFAULT config dir so an empty (or canonically-default) configDir collapses to the default binding (top-level config). Pass a default provider (from Get), NOT a GetWithDir override — the override's StateDir() is the custom dir itself and would always look "default".
func DetectInstalled ¶
func DetectInstalled() []string
DetectInstalled returns the canonical names of providers a user actually uses — those whose CLI binary is on PATH OR whose state/config dir is present (desktop-app or CLI-uninstalled installs) — in fixed registry order. Each provider appears at most once. Result is never nil but may be empty.
func MaterializeOpenCodeSession ¶ added in v0.17.3
func MaterializeOpenCodeSession(ctx context.Context, source ocSource, sessionID, outputPath string, interval time.Duration) (int, error)
Materialize runs a single seed + reconcile pass for one OpenCode session, writing every currently-complete message to outputPath, and returns the number of lines appended on this pass. It is the offline counterpart to Run (no ticker, no parent process): `confab save --provider opencode` calls it to produce the materialized JSONL the file-based sync engine then uploads.
Reuses the collector's reconcile — same completeness gating, ULID ordering, stop-at-first-incomplete, and idempotent re-seed from any existing file — so offline and live materialization can never diverge. interval is only used as the warn cadence floor; pass 0 for the default.
func NormalizeName ¶
NormalizeName returns the canonical provider name. Backed by the registry so it can't drift from the Provider list. An empty name returns ErrNoProvider via Get (no implicit claude-code fallback; kata frm7).
func OpenCodeDBPath ¶ added in v0.17.0
OpenCodeDBPath resolves the OpenCode SQLite DB path in this order:
- CONFAB_OPENCODE_DB env override
- $XDG_DATA_HOME/opencode/opencode.db (when XDG_DATA_HOME is set)
- ~/.local/share/opencode/opencode.db
The returned path is not guaranteed to exist on disk; callers handle that via the reader's normal retry path.
func OpencodeChildBackendName ¶ added in v0.17.0
OpencodeChildBackendName returns the path-encoded backend file_name a daemon registrar should use when registering an OpenCode child file with the tracker. Forward slashes are load-bearing (the backend parses the path segments to resolve the child session id).
func OrderedNames ¶ added in v0.17.0
func OrderedNames() []string
OrderedNames returns the canonical provider names in fixed registry order. Callers that render or detect per-provider iterate this instead of re-hardcoding the list. Returns a fresh copy each call.
func ResetStateDBPathCacheForTest ¶
func ResetStateDBPathCacheForTest()
ResetStateDBPathCacheForTest clears the cached state DB path so the next StateDBPath call re-evaluates the env var and glob. Production code resolves the path once per process; tests need to swap fixtures between cases. Lives in non-test code so cross-package tests (sync, daemon, cmd) can call it.
func ResetWalkUpRetryForTest ¶
func ResetWalkUpRetryForTest()
ResetWalkUpRetryForTest restores production retry defaults.
func SetWalkUpRetryForTest ¶
SetWalkUpRetryForTest shrinks WalkUpToRoot's retry budget for tests that would otherwise wait for the full production timeout. Pair with ResetWalkUpRetryForTest in t.Cleanup.
func TruncateUTF8 ¶ added in v0.17.0
TruncateUTF8 returns s truncated so its byte length is at most maxBytes, without splitting a multi-byte rune. If truncation occurs and maxBytes >= 3, appends "..." (3 ASCII bytes) to indicate continuation. Returns s unchanged if len(s) <= maxBytes. Returns "" when maxBytes <= 0.
Types ¶
type AnnotationResult ¶
type AnnotationResult struct {
IncludedFirstUserMessage bool
SummaryLinks []SummaryLink
}
AnnotationResult is the structured return from AnnotateChunk. IncludedFirstUserMessage tells the engine whether to flip its sentFirstUserMessage flag. SummaryLinks (Claude only) tells the engine which parent summaries to link via the backend.
type ChunkView ¶
type ChunkView interface {
FileType() string
FirstLine() int
Lines() []string
// FilePath is the absolute path of the source file this chunk was read
// from. Cursor uses it to stat the transcript mtime (latest_message_at)
// and to derive the session id for the CLI meta.json title lookup.
FilePath() string
FileCodexRollout() *CodexRolloutMetadata
SetCodexRolloutMetadata(*CodexRolloutMetadata)
SetSummary(string)
SetFirstUserMessage(string)
// SetLatestMessageAt records an explicit session timestamp on the chunk
// metadata (Cursor only — its JSONL lines have no per-line timestamp).
SetLatestMessageAt(time.Time)
}
ChunkView is the structural view of a chunk + its source file that AnnotateChunk reads from and writes back into. pkg/sync's chunkView adapter satisfies it. Setters mutate the underlying chunk's metadata in place; accessors return read-only snapshots.
type ClaudeCode ¶
type ClaudeCode struct {
// contains filtered or unexported fields
}
ClaudeCode contains Claude Code-specific local behavior. configDirOverride, when non-empty, takes precedence over CONFAB_CLAUDE_DIR and the default in StateDir() — it is how `confab setup --provider claude-code --config-dir <dir>` retargets installation into a non-default config dir (kata hpec). The zero value (ClaudeCode{}) keeps today's behavior.
func (ClaudeCode) AnnotateChunk ¶
func (p ClaudeCode) AnnotateChunk(c ChunkView, _ bool, redact func(string) string) AnnotationResult
AnnotateChunk extracts the local summary, first user message, and summary-link records from a Claude Code transcript chunk. Summary links are returned via AnnotationResult.SummaryLinks so the engine can perform the backend HTTP after AnnotateChunk returns — keeping the provider side-effect-free.
Non-transcript files are a no-op (Claude extracts only from transcripts).
Claude does not gate first-user-message extraction on a "first time" flag — the discovery helper handles dedup internally and the engine historically does not flip sentFirstUserMessage for Claude. The returned IncludedFirstUserMessage stays false so the engine's flag is untouched.
func (ClaudeCode) CLIBinaryName ¶
func (ClaudeCode) CLIBinaryName() string
CLIBinaryName returns "claude" — the binary `claude` users install via the Claude Code installer.
func (ClaudeCode) ConfigDirFromTranscript ¶ added in v0.17.1
func (ClaudeCode) ConfigDirFromTranscript(transcriptPath string) (string, error)
ConfigDirFromTranscript derives the Claude config dir from a transcript path. Claude writes transcripts to <configDir>/projects/<enc-cwd>/<id>.jsonl, so the config dir is the directory three levels above the file — but only when the path actually has that shape (absolute, and the segment two levels up is literally "projects", which anchors on the LAST "projects" so a config dir whose own path contains "projects" still resolves correctly). The result is canonicalized so it matches a stored binding key. Returns an error when the path does not have the expected layout, so callers can fall back to the default binding.
func (ClaudeCode) DefaultCWD ¶
func (p ClaudeCode) DefaultCWD(transcriptPath string) string
func (ClaudeCode) DiscoverDescendants ¶
func (ClaudeCode) DiscoverDescendants(DescendantRegistrar, string) error
DiscoverDescendants is a no-op for Claude Code. Claude's agent files are discovered transitively from transcript content (agent IDs embedded in JSONL messages) inside tracker.DiscoverNewFiles — no external state DB lookup is required.
func (ClaudeCode) DiscoverWorkflowFiles ¶ added in v0.16.2
func (ClaudeCode) DiscoverWorkflowFiles(reg WorkflowRegistrar, allow func(fileType string) bool) (int, error)
DiscoverWorkflowFiles implements provider.Provider — see the interface doc.
func (ClaudeCode) ExtractAgentIDsFromMessage ¶
func (ClaudeCode) ExtractAgentIDsFromMessage(message map[string]interface{}) []string
ExtractAgentIDsFromMessage extracts agent IDs from a parsed Claude transcript message. Checks both root-level toolUseResult.agentId and nested content blocks. Empty slice on non-user messages or missing fields.
func (ClaudeCode) ExtractMetadata ¶
func (p ClaudeCode) ExtractMetadata(lines []string) SessionMetadata
ExtractMetadata parses summary, first user message, and summary links from in-memory Claude transcript lines. Lines beyond maxLinesForExtraction are ignored.
For summaries:
- Entries with a leafUuid go to SummaryLinks (links to previous sessions).
- Entries without leafUuid become the local Summary (last one wins).
For user messages: the first one encountered sets FirstUserMessage.
func (ClaudeCode) FindParentPID ¶
func (p ClaudeCode) FindParentPID() int
FindParentPID walks up the process tree to find the Claude Code process.
func (ClaudeCode) FindSessionByID ¶
func (p ClaudeCode) FindSessionByID(partialID string) (string, string, error)
FindSessionByID resolves a full or partial Claude session ID to its full ID and transcript path. Walk-up is identity for Claude (no thread tree). Returns an error on no-match or ambiguous prefix.
func (ClaudeCode) InitTranscript ¶
func (ClaudeCode) InitTranscript(TranscriptRegistrar, string, string) error
InitTranscript is a no-op for Claude Code — there is no root rollout metadata to attach (Codex-only concern).
func (ClaudeCode) InstallHooks ¶
func (p ClaudeCode) InstallHooks() (string, error)
InstallHooks installs all four Confab hook bundles (sync, PreToolUse, PostToolUse, UserPromptSubmit). Returns the settings.json path.
func (ClaudeCode) InstallSkills ¶
func (p ClaudeCode) InstallSkills() error
InstallSkills installs the Claude Code skills shipped with confab (/retro) and prunes any retired skills left by older versions.
func (ClaudeCode) IsHooksInstalled ¶
func (p ClaudeCode) IsHooksInstalled() (bool, error)
IsHooksInstalled reports whether all four Confab hook bundles for Claude Code are installed. Mirrors InstallHooks: true only when every bundle is present.
func (ClaudeCode) IsProcess ¶
func (p ClaudeCode) IsProcess(pid int) bool
IsProcess checks if the given PID is a Claude Code process.
func (ClaudeCode) IsSkillInstalled ¶
func (p ClaudeCode) IsSkillInstalled(name string) bool
IsSkillInstalled reports whether a shipped Claude Code skill exists.
func (ClaudeCode) MatchesProcess ¶
func (ClaudeCode) MatchesProcess(cmd string) bool
MatchesProcess checks if a command string matches Claude Code.
func (ClaudeCode) Name ¶
func (ClaudeCode) Name() string
Name returns the canonical Claude Code provider name.
func (ClaudeCode) OnAlreadyRunning ¶ added in v0.17.0
func (ClaudeCode) OnAlreadyRunning(string)
DefaultCWD returns filepath.Dir(transcriptPath); Claude has no richer per-session CWD source. OnAlreadyRunning is a no-op for Claude Code: hook deduplication is the normal path (Claude fires SessionStart on every turn). See Provider interface doc for the OpenCode-specific behavior.
func (ClaudeCode) ParseSessionHook ¶
func (p ClaudeCode) ParseSessionHook(r io.Reader) (HookInput, error)
ParseSessionHook reads a Claude SessionStart hook payload and returns the provider-agnostic view.
func (ClaudeCode) ProjectsDir ¶
func (p ClaudeCode) ProjectsDir() (string, error)
ProjectsDir returns the Claude projects directory.
func (ClaudeCode) ReadHookInput ¶
func (ClaudeCode) ReadHookInput(r io.Reader) (*types.ClaudeHookInput, error)
ReadHookInput reads and validates Claude hook JSON.
func (ClaudeCode) ReadSessionHookInput ¶
func (p ClaudeCode) ReadSessionHookInput(r io.Reader) (*types.ClaudeHookInput, error)
ReadSessionHookInput reads Claude session hook JSON and validates transcript_path.
func (ClaudeCode) ScanSessions ¶
func (p ClaudeCode) ScanSessions() ([]SessionInfo, error)
ScanSessions walks ~/.claude/projects/ and returns all user sessions sorted oldest first. Permission errors per path are reported to stderr and don't fail the scan.
func (ClaudeCode) SettingsPath ¶
func (p ClaudeCode) SettingsPath() (string, error)
SettingsPath returns the Claude settings file path.
func (ClaudeCode) ShouldSpawnForInput ¶
func (ClaudeCode) ShouldSpawnForInput(HookInput) bool
ShouldSpawnForInput is unconditional for Claude Code.
func (ClaudeCode) StateDir ¶
func (p ClaudeCode) StateDir() (string, error)
StateDir returns the Claude config/install directory. Precedence: configDirOverride (set via GetWithDir for `setup --config-dir`) > the CONFAB_CLAUDE_DIR env var > the default ~/.claude.
func (ClaudeCode) SupportsCommitLinking ¶ added in v0.16.1
func (ClaudeCode) SupportsCommitLinking() bool
SupportsCommitLinking reports that Claude Code installs PreToolUse + PostToolUse hooks that drive bidirectional GitHub linking.
func (ClaudeCode) UninstallHooks ¶
func (p ClaudeCode) UninstallHooks() (string, error)
UninstallHooks removes all four Confab hook bundles. Returns the settings.json path even if no hooks were present.
func (ClaudeCode) UninstallSkills ¶
func (p ClaudeCode) UninstallSkills() error
UninstallSkills removes the Claude Code skills shipped with confab.
func (ClaudeCode) ValidateTranscriptPath ¶
func (p ClaudeCode) ValidateTranscriptPath(path string) error
ValidateTranscriptPath checks that a Claude transcript path is safe: - Must be absolute - Must not contain ".." components - Must resolve to a location under the Claude projects directory
func (ClaudeCode) WalkUpToRoot ¶
func (ClaudeCode) WalkUpToRoot(sessionID string) (string, string, error)
WalkUpToRoot is the identity walk for Claude Code: there is no thread tree, so the firing session is always its own root and rootPath is "".
func (ClaudeCode) WriteHookResponse ¶
WriteHookResponse writes a ClaudeHookResponse to w.
type Codex ¶
type Codex struct{}
func (Codex) AnnotateChunk ¶
func (p Codex) AnnotateChunk(c ChunkView, sentFirstUserMessage bool, redact func(string) string) AnnotationResult
AnnotateChunk attaches Codex-specific chunk metadata. Two concerns are handled independently:
first_user_message: extracted via ExtractMetadata from the root transcript's chunks (Codex emits the user prompt once at the start). Gated by c.FileType() == "transcript" + !sentFirstUserMessage. The closure `redact` (nil-safe) is applied before attaching.
codex_rollout: per-rollout metadata so the backend can upsert the `codex_rollouts` row. Emitted on the FIRST chunk of any Codex rollout (root or descendant) — detected via c.FirstLine() == 1. No persistent state flag is needed; retries preserve FirstLine == 1. Backend upsert is idempotent.
func (Codex) CLIBinaryName ¶
CLIBinaryName returns "codex" — the binary users install via Codex.
func (Codex) ConfigPath ¶
func (Codex) DefaultCWD ¶
DefaultCWD reads session_meta.cwd from the rollout. Falls back to filepath.Dir on read failure or empty CWD so the upload still has a directory to record.
func (Codex) DiscoverDescendants ¶
func (p Codex) DiscoverDescendants(reg DescendantRegistrar, rootThreadUUID string) error
DiscoverDescendants queries the local Codex SQLite state DB for every descendant of rootThreadUUID, verifies each rollout file exists and looks like an actual subagent (ValidateRolloutPath + !IsUserSession check on session_meta), and registers verified ones via reg.RegisterCodexRollout.
Idempotent across calls (skips already-tracked filenames). Degrades gracefully when the state DB is missing or its schema doesn't match — ListSubtree returns (nil, nil) and we return nil. Per-descendant verification failures log at warn level and skip the row.
func (Codex) DiscoverWorkflowFiles ¶ added in v0.16.2
DiscoverWorkflowFiles is a no-op for Codex: workflow subagents are a Claude Code harness feature (the Workflow tool) with no Codex equivalent, so there are never workflow run dirs to scan. The allow predicate is intentionally never invoked, so a Codex session never triggers a backend capability probe.
func (Codex) ExtractFirstUserMessageFromLines ¶
ExtractFirstUserMessageFromLines returns the first non-empty user message found in the given rollout lines, truncated to MaxMetadataFieldLength/2 bytes on a UTF-8 boundary. Returns "" when no user message is present.
func (Codex) ExtractMetadata ¶
func (p Codex) ExtractMetadata(lines []string) SessionMetadata
ExtractMetadata returns the in-memory metadata for a Codex chunk. Summary and SummaryLinks stay empty — those are Claude-only concepts. Lines are capped to maxLinesForExtraction to mirror Claude's bound.
func (Codex) FindParentPID ¶
FindParentPID walks up the process tree to find the Codex process. Mirrors ClaudeCode.FindParentPID for daemon parent-liveness monitoring.
func (Codex) FindSessionByID ¶
FindSessionByID resolves a full or partial UUID to the ROOT thread's UUID and rollout path. If partialID matches a subagent, this walks up to the top-most user session via WalkUpToRoot so callers transparently upload the whole tree.
func (Codex) InitTranscript ¶
func (p Codex) InitTranscript(target TranscriptRegistrar, transcriptPath, externalID string) error
InitTranscript reads the root rollout's session_meta and attaches the resulting CodexRolloutMetadata to the transcript file so the very first uploaded chunk carries codex_rollout metadata. On read failure (missing or malformed rollout file) logs at warn level and still attaches the bare-minimum metadata (ThreadUUID + RolloutPath) so the backend can upsert a row; CWD/Model/etc. stay empty. Matches pre-CF-397 behavior. Never returns an error — failure modes are recoverable.
func (Codex) InstallHooks ¶
InstallHooks delegates to pkg/hookconfig.
func (Codex) InstallSkills ¶
InstallSkills installs the Codex skills shipped with confab (/retro) and prunes any retired skills left by older versions.
func (Codex) IsHooksInstalled ¶
IsHooksInstalled delegates to pkg/hookconfig, which parses ~/.codex/config.toml and returns true iff a confab command is registered under [[hooks.SessionStart]].
func (Codex) IsSkillInstalled ¶
IsSkillInstalled reports whether a shipped Codex skill exists.
func (Codex) ListSubtree ¶
func (p Codex) ListSubtree(rootThreadUUID string) ([]CodexThreadRow, error)
ListSubtree returns every descendant of rootThreadUUID, at any depth, via a recursive CTE over `thread_spawn_edges` JOIN `threads`. Each returned row's ParentThreadUUID is the immediate parent (which may itself be a descendant of rootThreadUUID for grandchildren). The root itself is NOT returned.
Returns (nil, nil) when the DB doesn't exist or the schema doesn't match — degrades gracefully so daemon sync cycles continue uninterrupted.
func (Codex) MatchesProcess ¶
MatchesProcess checks if a command string matches a Codex invocation.
func (Codex) OnAlreadyRunning ¶ added in v0.17.0
OnAlreadyRunning is a no-op for Codex: hook deduplication is the normal path (Codex fires SessionStart for every spawned subagent). See Provider interface doc for the OpenCode-specific behavior.
func (Codex) ParseSessionHook ¶
ParseSessionHook reads a Codex SessionStart hook payload and returns the provider-agnostic view.
func (Codex) ReadHookInput ¶
func (Codex) ReadSessionHookInput ¶
func (Codex) ReadSessionInfo ¶
func (p Codex) ReadSessionInfo(path string) (CodexSessionInfo, error)
ReadSessionInfo reads a rollout's session_meta line and returns the parsed CodexSessionInfo (with file stat info populated).
func (Codex) ScanCodexSessions ¶
func (p Codex) ScanCodexSessions() ([]CodexSessionInfo, error)
ScanCodexSessions returns the rich Codex-specific session info for every user-initiated rollout. Internal callers that need CodexSessionInfo's extras (CWD, Model, AgentRole, ...) use this directly; the cross-provider Provider.ScanSessions interface method projects to []SessionInfo.
func (Codex) ScanSessions ¶
func (p Codex) ScanSessions() ([]SessionInfo, error)
ScanSessions projects ScanCodexSessions to the cross-provider SessionInfo shape. FirstUserMessage is extracted from each rollout's first event_msg.user_message line (capped to maxLinesForExtraction). Sessions are returned oldest first to match Claude's ordering.
func (Codex) SessionIDFromRolloutPath ¶
SessionIDFromRolloutPath parses the UUID embedded in a Codex rollout filename. Returns ("", false) on filenames that don't match the rollout pattern.
func (Codex) SessionsDir ¶
func (Codex) ShouldSpawnForInput ¶
ShouldSpawnForInput inspects the rollout file's session_meta to decide whether the firing hook represents a user-initiated rollout (true) or a subagent (false).
func (Codex) StateDBPath ¶
StateDBPath resolves the path to Codex's local state SQLite DB. Resolution order:
- CONFAB_CODEX_STATE_DB env var (escape hatch for tests/debugging).
- Glob `<StateDir>/state_*.sqlite`, parse the integer suffix between `_` and `.sqlite`, return the entry with the highest numeric suffix. If suffixes don't parse as integers, falls back to alphabetical max.
- Returns os.ErrNotExist if no candidate file exists.
The result is cached for the lifetime of the process via sync.Once.
func (Codex) SupportsCommitLinking ¶ added in v0.16.1
SupportsCommitLinking reports that Codex installs PreToolUse + PostToolUse hooks that drive bidirectional GitHub linking. See pkg/hookconfig/codex.go for the managed-block contents.
func (Codex) UninstallHooks ¶
UninstallHooks delegates to pkg/hookconfig.
func (Codex) UninstallSkills ¶
UninstallSkills removes the Codex skills shipped with confab.
func (Codex) ValidateRolloutPath ¶
func (Codex) WalkUpToRoot ¶
WalkUpToRoot walks the parent_thread_id chain in `thread_spawn_edges` starting from threadUUID until it reaches a thread with no parent edge. Returns the top-most root's UUID + that root's rollout_path.
If threadUUID is already a root (no parent edge), returns it unchanged. If the DB is unavailable or the thread isn't in the DB at all, returns (threadUUID, "", nil) — graceful degradation so callers don't need to branch on transient errors.
Built-in retry: the FIRST parent lookup retries up to walkUpRetryAttempts times with walkUpRetryBackoff between attempts. This absorbs the spawn-vs-edge race where Codex fires the SessionStart hook for a fresh subagent before the `thread_spawn_edges` row has been committed.
The retry only fires on the first hop; subsequent hops (grandchild → child → root walks) assume the chain is already stable in Codex's DB.
Logs the retry-attempt count + wall time at info level so we can later tune the retry budget against observed race timing.
type CodexRolloutMetadata ¶
type CodexRolloutMetadata struct {
ThreadUUID string `json:"thread_uuid"`
ParentThreadUUID string `json:"parent_thread_uuid,omitempty"` // "" for roots
RolloutPath string `json:"rollout_path"`
CWD string `json:"cwd,omitempty"`
Model string `json:"model,omitempty"`
// Source is the flattened discriminator from Codex's polymorphic
// session_meta.source field — a short string like "cli" or "subagent".
// The backend's `codex_rollouts.source` column caps this at 64 chars.
Source string `json:"source,omitempty"`
ThreadSource string `json:"thread_source,omitempty"`
AgentPath string `json:"agent_path,omitempty"`
AgentRole string `json:"agent_role,omitempty"`
AgentNickname string `json:"agent_nickname,omitempty"`
}
CodexRolloutMetadata is the per-rollout metadata transmitted on the FIRST chunk of a Codex rollout (root or descendant). The backend upserts it into the `codex_rollouts` table keyed by ThreadUUID. Omitted on chunks where chunk.FirstLine != 1, so the backend handler treats absence as "no metadata to record this round."
Codex-only; the backend rejects this field on non-codex sessions with 400.
Lives in pkg/provider so both pkg/sync (wire format) and pkg/provider's Codex implementation can construct one without a package import cycle. pkg/sync re-exports it via `type CodexRolloutMetadata = provider.CodexRolloutMetadata`.
Redaction: fields below are sourced from Codex's session_meta (and the SQLite state DB for descendants) and ride on the first chunk unredacted. Rollout *content* is redacted in pkg/sync.FileTracker.ReadChunk; these metadata fields are not. Current fields are short, structured values (path, model name, agent role). Before adding a field that could carry free-text user content, plumb the redactor into Codex.InitTranscript / Codex.DiscoverDescendants rather than extending this struct.
type CodexSessionInfo ¶
type CodexSessionInfo struct {
SessionID string
RolloutPath string
CWD string
Model string
// Source is a short discriminator extracted from the rollout's `source`
// field. Codex writes that field as either a bare string ("cli") for
// user-initiated rollouts or a tagged object ({"subagent":{...}}) for
// spawned subagents. The string case is passed through; the object case
// is collapsed to its top-level key. Empty when session_meta omits the
// field. Matches the backend's 64-char `codex_rollouts.source` column.
Source string
ThreadSource string
AgentPath string
AgentRole string
AgentNickname string
ModTime time.Time
SizeBytes int64
}
CodexSessionInfo is the rich Codex-specific session metadata returned by ScanCodexSessions and ReadSessionInfo. Internal callers (cmd/save.go, engine init paths) need the extras (CWD, Model, AgentRole, ...) that don't fit on the cross-provider SessionInfo.
func (CodexSessionInfo) IsUserSession ¶
func (s CodexSessionInfo) IsUserSession() bool
IsUserSession reports whether this rollout is a top-level user session (not a subagent or memory rollout).
type CodexThreadRow ¶
type CodexThreadRow struct {
ThreadUUID string
ParentThreadUUID string
RolloutPath string
CWD string
Model string
Source string
ThreadSource string
AgentPath string
AgentRole string
AgentNickname string
}
CodexThreadRow describes a single Codex thread as recorded in the local state DB's `threads` table. ParentThreadUUID is the immediate parent recorded in `thread_spawn_edges`, or "" for a root thread.
type Cursor ¶ added in v0.17.3
type Cursor struct{}
Cursor contains Cursor-specific local behavior (cursor-agent CLI + Cursor desktop IDE). This T2 scope covers the core provider: registration, types, path derivation, and process matching. Hook installation, daemon wiring, transcript parsing, and subagent capture are fleshed out in T3–T6.
func (Cursor) AnnotateChunk ¶ added in v0.17.3
AnnotateChunk annotates transcript chunks with the session metadata the backend needs for Cursor (spm9). On every transcript chunk it sets:
- first_user_message — so the session is listable (the backend hides sessions with neither a summary nor a first_user_message; kata kk5t).
- latest_message_at — from the transcript file's mtime. Cursor JSONL lines carry NO per-line timestamp, so the backend opts Cursor out of per-line extraction and feeds session.last_message_at SOLELY from this field. The mtime is universal across the CLI and IDE.
- summary — from the CLI meta.json title when present (CLI-only; absent for IDE sessions, which keep first_user_message alone). Best-effort.
Non-transcript (agent sidechain) chunks are a no-op. IncludedFirstUserMessage stays false so the engine's sentFirstUserMessage flag is untouched (same as Claude). The model is set engine-side from the daemon config (sourced from the sessionStart hook), not here. Every read here is best-effort: a missing file or meta.json never errors the chunk.
func (Cursor) CLIBinaryName ¶ added in v0.17.3
CLIBinaryName returns "cursor-agent" — the CLI binary users install.
func (Cursor) DefaultCWD ¶ added in v0.17.3
DefaultCWD returns filepath.Dir(transcriptPath). Cursor's transcript lives at .../agent-transcripts/<id>/<id>.jsonl, so this is the per-session directory.
func (Cursor) DiscoverDescendants ¶ added in v0.17.3
func (Cursor) DiscoverDescendants(reg DescendantRegistrar, _ string) error
DiscoverDescendants scans <root-dir>/subagents/ each SyncAll cycle and registers every *.jsonl file there as a file_type=agent sidechain. Idempotent: RegisterSidechainFile returns false for an already-tracked file, so re-scanning every cycle costs only a directory read. A no-op when the registrar lacks the sidechain surface, when the root transcript path is unavailable, or when the subagents directory does not exist (the common case).
func (Cursor) DiscoverWorkflowFiles ¶ added in v0.17.3
DiscoverWorkflowFiles is a no-op: Cursor has no Workflow-tool equivalent.
func (Cursor) ExtractMetadata ¶ added in v0.17.3
func (Cursor) ExtractMetadata(lines []string) SessionMetadata
ExtractMetadata parses the first user message from in-memory Cursor transcript lines. Cursor's JSONL message lines are {role, message:{content}} with NO top-level "type" field; status lines are {type:"turn_ended", …} and carry no role (kata 6kys §7). FirstUserMessage is the first role=="user" line's first text part, with the <user_query>…</user_query> wrapper stripped. Summary stays empty (Cursor has no inline summary) and SummaryLinks stays nil (Cursor has none). Lines beyond maxLinesForExtraction are ignored.
func (Cursor) FindParentPID ¶ added in v0.17.3
FindParentPID walks up the process tree to find the Cursor process (CLI or IDE), mirroring ClaudeCode.FindParentPID. The IDE app process can be the grandparent of the hook (the hook is spawned by a "Cursor Helper (Plugin)" child of /Applications/Cursor.app), so the parent+grandparent walk suffices.
func (Cursor) FindSessionByID ¶ added in v0.17.3
FindSessionByID resolves a full or partial Cursor session ID to its full ID and transcript path (prefix match, mirroring the other providers). Walk-up is identity for Cursor (subagents fire their own hooks). Returns an error on no-match or ambiguous prefix.
func (Cursor) InitTranscript ¶ added in v0.17.3
func (Cursor) InitTranscript(TranscriptRegistrar, string, string) error
InitTranscript is a no-op for Cursor — no root rollout metadata to attach.
func (Cursor) InstallHooks ¶ added in v0.17.3
InstallHooks installs Confab's sessionStart (daemon spawn), sessionEnd (signal shutdown), and preToolUse/postToolUse (GitHub commit/PR linking; 65aq) hooks into ~/.cursor/hooks.json, preserving user hooks. Returns the written hooks.json path.
func (Cursor) InstallSkills ¶ added in v0.17.3
InstallSkills installs confab's bundled skills (/retro) into ~/.cursor/skills/. Cursor auto-loads global skills from <stateDir>/skills/ using the generic SKILL.md template (kata 6kys addendum 2), so the existing reconcile path works unchanged.
func (Cursor) IsHooksInstalled ¶ added in v0.17.3
IsHooksInstalled reports true only when both managed Cursor hook events (sessionStart + sessionEnd) carry a confab command.
func (Cursor) IsProcess ¶ added in v0.17.3
IsProcess reports whether pid is a Cursor process (CLI or IDE).
func (Cursor) IsSkillInstalled ¶ added in v0.17.3
IsSkillInstalled reports whether a shipped Cursor skill exists.
func (Cursor) MatchesProcess ¶ added in v0.17.3
MatchesProcess reports whether a command string matches a Cursor process.
func (Cursor) OnAlreadyRunning ¶ added in v0.17.3
OnAlreadyRunning is a no-op for Cursor: subagents never fire sessionStart, so an already-running hit is ordinary hook dedup, not an error.
func (Cursor) ParseSessionHook ¶ added in v0.17.3
ParseSessionHook reads a Cursor sessionStart-style hook payload and returns the provider-agnostic view. Because transcript_path is null at sessionStart, it derives the path from workspace_roots[0] + session_id so the standard launch path (which reads HookInput.TranscriptPath()) works unchanged. A payload that already carries an explicit transcript_path (e.g. sessionEnd) keeps it.
func (Cursor) ProjectsDir ¶ added in v0.17.3
ProjectsDir returns <stateDir>/projects, the root under which Cursor stores per-workspace transcripts.
func (Cursor) ReadHookInput ¶ added in v0.17.3
ReadHookInput reads and validates raw Cursor hook JSON: session_id required and safe; transcript_path NOT required (null at sessionStart, derived by ParseSessionHook). This is the non-strict reader used on the spawn path.
func (Cursor) ReadSessionHookInput ¶ added in v0.17.3
ReadSessionHookInput reads Cursor session hook JSON and additionally requires + validates transcript_path, mirroring ClaudeCode.ReadSessionHookInput. Used where a populated transcript_path is required (e.g. sessionEnd / offline flows); the spawn path uses ReadHookInput + path derivation instead.
func (Cursor) ScanSessions ¶ added in v0.17.3
func (p Cursor) ScanSessions() ([]SessionInfo, error)
ScanSessions walks <stateDir>/projects/*/agent-transcripts/*/<id>.jsonl and returns all user sessions sorted oldest first. Subagent sidechain files (nested under .../subagents/) are excluded — only the top-level transcript whose basename equals its parent directory name is a session. Cursor writes real transcript files, so offline `confab save <id>` is supported (unlike OpenCode, which has no on-disk file). Permission errors per path are reported to stderr and do not fail the scan.
func (Cursor) ShouldSpawnForInput ¶ added in v0.17.3
ShouldSpawnForInput is unconditional for Cursor: subagents never fire sessionStart, so there is no duplicate-daemon risk to suppress (kata 6kys).
func (Cursor) StateDir ¶ added in v0.17.3
StateDir returns the Cursor config/install directory. Precedence: CONFAB_CURSOR_DIR > the default ~/.cursor.
func (Cursor) SupportsCommitLinking ¶ added in v0.17.3
SupportsCommitLinking reports true: Cursor wires bidirectional GitHub commit/PR linking (65aq) via preToolUse (updated_input rewrite to inject the Confab-Link trailer / PR-body line) and postToolUse (link the resulting commit SHA / PR URL back to the session).
func (Cursor) UninstallHooks ¶ added in v0.17.3
UninstallHooks removes Confab's managed hook entries from ~/.cursor/hooks.json, preserving any user-authored hooks.
func (Cursor) UninstallSkills ¶ added in v0.17.3
UninstallSkills removes confab's bundled skills from ~/.cursor/skills/.
func (Cursor) ValidateTranscriptPath ¶ added in v0.17.3
ValidateTranscriptPath checks that a Cursor transcript path is safe, mirroring ClaudeCode.ValidateTranscriptPath: absolute, no ".." components, and under the Cursor projects directory.
func (Cursor) WalkUpToRoot ¶ added in v0.17.3
WalkUpToRoot is the identity walk for Cursor: subagents fire their own dedicated subagentStart/Stop hooks (never sessionStart), so the firing session is always its own root and rootPath is "".
func (Cursor) WriteHookResponse ¶ added in v0.17.3
WriteHookResponse writes an empty JSON object ({}) and relies on exit 0. Cursor's sessionStart response schema is {env?, additional_context?} and is fire-and-forget; confab injects no context, so the Claude/Codex response shape (and systemMessage) is intentionally NOT emitted. suppressOutput and systemMessage are ignored.
type DescendantRegistrar ¶
type DescendantRegistrar interface {
IsTracked(fileName string) bool
RegisterCodexRollout(path, fileName string, isRoot bool, meta CodexRolloutMetadata)
}
DescendantRegistrar is the surface DiscoverDescendants uses to register newly-discovered sidechain files. *sync.FileTracker satisfies it via IsTracked + RegisterCodexRollout.
type HookInput ¶
type HookInput interface {
SessionID() string
TranscriptPath() string
CWD() string
HookEventName() string
ParentPID() int
}
HookInput is the provider-agnostic view of a hook payload, exposing the fields used by daemon spawning and bookkeeping. Concrete shapes (types.ClaudeHookInput, types.CodexHookInput) satisfy this via adapter types defined in hookinput.go.
type OpenCodeCollector ¶ added in v0.17.0
type OpenCodeCollector struct {
// contains filtered or unexported fields
}
OpenCodeCollector materializes a single OpenCode session's messages into a local JSONL file that the ordinary file-based sync pipeline uploads.
OpenCode keeps session data in a local SQLite DB (one row per message, one row per part). The collector polls the DB on a ticker, fetches new messages strictly after the high-water mark, filters out non-terminal tool parts, walks in ULID order stopping at the first incomplete message (see ocIsComplete), and appends each emitted envelope as one line of JSON. The file stays append-only and monotonic — which is what pkg/sync's incremental line-offset tracking requires.
Idempotency across daemon restarts needs no extra persisted state: on start, seed re-builds the emitted-id set and the HWM from any existing output file.
func NewOpenCodeCollector ¶ added in v0.17.0
func NewOpenCodeCollector(source ocSource, sessionID, outputPath string, interval time.Duration) *OpenCodeCollector
NewOpenCodeCollector builds a collector for one session. interval is the poll cadence; pass 0 for a sensible default. The interval is wired from daemon.syncInterval so CONFAB_SYNC_INTERVAL_MS tunes both backend sync and SQLite polling with a single knob.
type OpenCodeDBReader ¶ added in v0.17.0
type OpenCodeDBReader struct {
// contains filtered or unexported fields
}
OpenCodeDBReader reads OpenCode session data from a local SQLite DB (~/.local/share/opencode/opencode.db). It is the only producer of the materialized {info, parts} JSONL the collector appends; everything downstream of that file is provider-agnostic.
Each ReadSession call opens the DB read-only, runs a single LEFT JOIN query, and closes — mirroring the Codex state-DB read pattern. The DB is concurrently written by OpenCode (WAL mode); the reader never writes, uses busy_timeout for transient lock contention, and tolerates seeing rows mid-write (downstream completeness gating in ocIsComplete handles that).
func NewOpenCodeDBReader ¶ added in v0.17.0
func NewOpenCodeDBReader(dbPath string) *OpenCodeDBReader
NewOpenCodeDBReader builds a reader bound to a specific DB path. The path is not validated until ReadSession runs.
func (*OpenCodeDBReader) FirstUserMessageText ¶ added in v0.17.3
func (r *OpenCodeDBReader) FirstUserMessageText(ctx context.Context, sessionID string) (string, error)
FirstUserMessageText returns the first user message's first text part for a session, used to populate the list TITLE column (OpenCode has no summary). Bounded: it reads only the small leading slice of the session needed to find the first user text, via a single LEFT JOIN over the user message with the lowest message id. Returns ("", nil) when there is no usable user text — the list TITLE is then blank, not an error.
firstUserMessageScanLimit caps how many leading messages are scanned so the secondary per-session read stays cheap even for long sessions.
func (*OpenCodeDBReader) ListDescendants ¶ added in v0.17.0
func (r *OpenCodeDBReader) ListDescendants(ctx context.Context, rootSessionID string) ([]string, error)
ListDescendants returns every descendant session ID under rootSessionID (at any depth), discovered via recursive walk of session.parent_id. The root itself is excluded; results are returned in ULID lex order (which equals chronological order for OpenCode session ids) so callers see a deterministic enumeration. Capped at listDescendantsLimit rows.
Returns an error only when the DB file is unreadable — callers (the provider's DiscoverDescendants) translate that to a Warn log + nil so the daemon's sync cycle continues uninterrupted past a transient DB-absence.
func (*OpenCodeDBReader) ListRootSessions ¶ added in v0.17.3
func (r *OpenCodeDBReader) ListRootSessions(ctx context.Context) ([]OpenCodeRootSession, error)
ListRootSessions enumerates root sessions (parent_id IS NULL), newest first, for offline `confab list --provider opencode`. Children are excluded, mirroring the daemon's root-only spawn rule. Returns a clear error when the DB is missing/unreadable so the list command can report it (manual commands surface DB errors rather than the daemon's Warn-and-continue).
func (*OpenCodeDBReader) MatchSessionIDs ¶ added in v0.17.3
MatchSessionIDs returns every session id (root or descendant) whose id has partialID as a prefix, ordered for determinism. Used by FindSessionByID to resolve a partial id the way the file-based providers do. An empty partialID matches everything (the caller then reports ambiguity unless there is exactly one session).
func (*OpenCodeDBReader) ReadSession ¶ added in v0.17.0
func (r *OpenCodeDBReader) ReadSession(ctx context.Context, sessionID, sinceMessageID string) ([]ocRawEnvelope, error)
ReadSession returns messages for sessionID strictly greater than sinceMessageID (pass "" for a full read), as raw {info, parts} envelopes ordered by (message.time_created, message.id) and by part.id within each message. The returned envelopes carry id/sessionID injected into info, and id/sessionID/messageID injected into each part — these live in DB columns, not in the stored JSON, so reconstruction is essential for the wire shape the materialized JSONL contract requires.
Returns (nil, nil) when the session has no qualifying rows yet (treated as "wait, retry" by the caller). Returns a clear error when the DB file is missing or unreadable.
func (*OpenCodeDBReader) ReadSessionInfo ¶ added in v0.17.0
func (r *OpenCodeDBReader) ReadSessionInfo(ctx context.Context, sessionID string) (directory, parentID string, err error)
ReadSessionInfo fetches a session row's directory and parent_id from the OpenCode SQLite DB. Returns empty strings (not an error) when the row is absent so the caller can proceed with best-effort defaults. Errors are returned only when the DB itself is unreadable.
Used by the resume path in cmd/hook_sessionstart.go to resolve the cwd + parent session id from a session_id-only payload (CF-549).
func (*OpenCodeDBReader) ResolveOpencodeRoot ¶ added in v0.17.3
func (r *OpenCodeDBReader) ResolveOpencodeRoot(ctx context.Context, sessionID string) (string, error)
ResolveOpencodeRoot walks session.parent_id up from sessionID to the topmost root (parent_id IS NULL) and returns its id. A session that is already a root returns itself. Bounded by opencodeRootWalkLimit as a cycle defense. Used by FindSessionByID so passing any descendant id resolves to the user-facing root (consistent with the root+descendants save scope).
type OpenCodeRootSession ¶ added in v0.17.3
type OpenCodeRootSession struct {
ID string
Directory string
TimeCreated int64 // unix epoch seconds (session.time_created)
}
OpenCodeRootSession is one row returned by ListRootSessions: the minimal columns offline session discovery (ScanSessions) needs to build a SessionInfo. FirstUserMessage is fetched separately (a bounded secondary read) because it lives in the message/part tables, not the session row.
type Opencode ¶ added in v0.17.0
type Opencode struct{}
func (Opencode) AnnotateChunk ¶ added in v0.17.0
func (Opencode) AnnotateChunk(c ChunkView, sentFirstUserMessage bool, redact func(string) string) AnnotationResult
AnnotateChunk sets first_user_message on the first transcript chunk so synced OpenCode sessions appear in the web session list (CF-540) — the backend's list query hides any session with neither a summary nor a first_user_message, and the CLI is the only source for those fields. OpenCode has no summary concept, so only first_user_message is set (mirroring Codex). The text is the first user message's first text part, trimmed and redacted (redact is nil-safe). A malformed materialized line degrades to "no message found" rather than failing the sync — we wrote these lines ourselves, so a parse error signals a collector bug worth a debug log, not a blocked upload.
func (Opencode) CLIBinaryName ¶ added in v0.17.0
func (Opencode) DefaultCWD ¶ added in v0.17.0
func (Opencode) DiscoverDescendants ¶ added in v0.17.0
func (Opencode) DiscoverDescendants(reg DescendantRegistrar, externalID string) error
DiscoverDescendants enumerates every OpenCode subagent session under externalID via the local SQLite DB and registers each as a path-encoded sidechain file under the root's backend session (CF-538). The reg must implement OpencodeDescendantRegistrar (the daemon-supplied wrapper that drives child-collector goroutine spawn). When the assertion misses (forgotten daemon setter, or unit tests using a plain *FileTracker), logs a Warn once and returns nil — the production path requires the wrapper; the log surfaces a misconfiguration that would otherwise be silent.
Per-tick semantics:
- Resolve the DB path (CONFAB_OPENCODE_DB env, then $XDG_DATA_HOME, then ~/.local/share). Provider is stateless: a fresh reader per call costs one stat + open.
- Recursive CTE walks session.parent_id descendants (capped at 1000 rows as a cycle defense).
- For each descendant: derive the nested local materialized path ~/.confab/opencode/<root>/children/<child>/messages.jsonl, then call reg.RegisterOpencodeChild. The registrar handles capability gating, file registration, and collector spawn — all idempotent.
DB unavailable → log Warn, return nil (consistent with Codex's behavior when its state DB is missing). The daemon's sync cycle continues uninterrupted past a transient DB-absence.
func (Opencode) DiscoverWorkflowFiles ¶ added in v0.17.0
func (Opencode) ExtractMetadata ¶ added in v0.17.0
func (Opencode) ExtractMetadata([]string) SessionMetadata
func (Opencode) FindParentPID ¶ added in v0.17.0
func (Opencode) FindSessionByID ¶ added in v0.17.0
FindSessionByID resolves a full or partial OpenCode session id to its full ROOT id, materializes the root's transcript to ~/.confab/opencode/<root>/messages.jsonl, and returns (rootID, path). A descendant id resolves up to its root (consistent with the root+descendants save scope); descendants themselves are materialized + registered later by the save path (saveOpencodeDescendants). Prefix match mirrors the other providers: a unique prefix is required (ambiguous → error). Honors CONFAB_OPENCODE_DB; a missing/unreadable DB returns a clear error.
func (Opencode) InitTranscript ¶ added in v0.17.0
func (Opencode) InitTranscript(TranscriptRegistrar, string, string) error
func (Opencode) InstallHooks ¶ added in v0.17.0
func (Opencode) InstallSkills ¶ added in v0.17.0
func (Opencode) IsHooksInstalled ¶ added in v0.17.0
func (Opencode) IsSkillInstalled ¶ added in v0.17.0
func (Opencode) OnAlreadyRunning ¶ added in v0.17.0
OnAlreadyRunning logs a warning that a parallel opencode process resumed the same session — a real edge case that confab's lifecycle model does not currently support reliably (CF-549, M2). The log goes to the confab log file only, not to opencode's stderr.
func (Opencode) ParseSessionHook ¶ added in v0.17.0
func (Opencode) ReadSessionHookInput ¶ added in v0.17.0
func (Opencode) ScanSessions ¶ added in v0.17.0
func (Opencode) ScanSessions() ([]SessionInfo, error)
ScanSessions enumerates OpenCode root sessions from the local SQLite DB for `confab list --provider opencode` (t6d5). Children are excluded (parent_id IS NULL), mirroring the daemon's root-only rule. Each row maps to a SessionInfo whose TITLE source is the first user message (OpenCode has no summary), read per-session via a bounded secondary query. The DB is opened read-only and honors CONFAB_OPENCODE_DB; a missing/unreadable DB returns a clear error.
TranscriptPath is left empty here: OpenCode has no on-disk transcript, so the path is produced lazily by FindSessionByID (which materializes on demand).
func (Opencode) ShouldSpawnForInput ¶ added in v0.17.0
ShouldSpawnForInput refuses subagent (non-root) OpenCode sessions so only the user-initiated root session spawns a daemon; CF-538 will capture subagents as sidechain files under the root. A session is a subagent when the plugin forwarded a parent session id (surfaced via an optional SessionParentID() accessor on the input — kept off the shared HookInput interface so Claude/ Codex inputs need not implement it). Inputs without the accessor (or with an empty parent id) are treated as root.
func (Opencode) SupportsCommitLinking ¶ added in v0.17.0
func (Opencode) UninstallHooks ¶ added in v0.17.0
func (Opencode) UninstallSkills ¶ added in v0.17.0
func (Opencode) WalkUpToRoot ¶ added in v0.17.0
type OpencodeDescendantRegistrar ¶ added in v0.17.0
type OpencodeDescendantRegistrar interface {
DescendantRegistrar
// RegisterOpencodeChild registers the child file (path-encoded backend
// file_name = "opencode/<childID>/messages.jsonl", file_type = "agent")
// AND ensures a collector goroutine is running for it. Idempotent: a
// repeat call for an already-tracked + already-collecting child is a
// no-op. Capability-gated internally on opencode_subagent_files; when
// the capability is off, both register and spawn no-op silently (the
// engine logs the capability state once via resolveCaps).
RegisterOpencodeChild(childSessionID, localPath string)
}
OpencodeDescendantRegistrar is the surface Opencode.DiscoverDescendants uses to register subagent sessions discovered in OpenCode's local SQLite (CF-538). The daemon supplies an implementation that wraps *FileTracker, performing capability-gated child registration AND idempotent collector goroutine spawn in one call.
*FileTracker does NOT satisfy this interface directly (no collector goroutine concept); the daemon's opencodeRegistrar wrapper does.
type Provider ¶
type Provider interface {
Name() string
// CLIBinaryName is the OS-level binary name to look up via
// exec.LookPath when detecting whether the provider is installed
// locally (e.g. "claude" for Claude Code, "codex" for Codex).
CLIBinaryName() string
StateDir() (string, error)
FindParentPID() int
IsProcess(pid int) bool
// SupportsCommitLinking reports whether this provider's hook system
// can drive bidirectional GitHub linking (commit-trailer + PR-body
// injection via PreToolUse; commit/PR URL linking via PostToolUse).
// Used by cmd/ handlers to silently no-op for providers that don't
// install those events.
SupportsCommitLinking() bool
// ParseSessionHook reads a SessionStart-style hook payload from r and
// returns the provider-agnostic view.
ParseSessionHook(r io.Reader) (HookInput, error)
InstallHooks() (string, error)
UninstallHooks() (string, error)
IsHooksInstalled() (bool, error)
// InstallSkills installs the bundled skills for this provider's local
// skill layout.
InstallSkills() error
UninstallSkills() error
IsSkillInstalled(name string) bool
// WalkUpToRoot returns the root session ID and its rollout path. For
// providers without a separate root file identifier (Claude Code),
// rootPath is "".
WalkUpToRoot(sessionID string) (rootID, rootPath string, err error)
// ShouldSpawnForInput is the per-provider gate on whether a fresh
// SessionStart should result in a daemon. Codex returns false for
// subagent rollouts; Claude is always true.
ShouldSpawnForInput(in HookInput) bool
// WriteHookResponse writes a hook response payload to w. The response
// shape is provider-specific but the (continue, suppressOutput,
// systemMessage) tuple is shared.
WriteHookResponse(w io.Writer, suppressOutput bool, systemMessage string) error
// InitTranscript is called from sync.Engine.Init AFTER the backend has
// returned the initial sync state and the transcript file has been
// registered in the tracker. Codex reads session_meta and attaches
// codex_rollout metadata to the root transcript so the first chunk
// uploaded carries it. Claude is a no-op. The engine logs+continues on
// error; implementations may return one for true I/O failures.
InitTranscript(target TranscriptRegistrar, transcriptPath, externalID string) error
// DiscoverDescendants is called once per SyncAll cycle, BEFORE the BFS
// loop. Providers with an external discovery model (Codex: SQLite
// subtree walk) register newly-discovered sidechain files via reg.
// Must be idempotent across calls (skip already-tracked filenames).
// Claude is a no-op (its agents are discovered transitively from
// transcript content, handled in tracker.DiscoverNewFiles).
DiscoverDescendants(reg DescendantRegistrar, externalID string) error
// DiscoverWorkflowFiles is called once per SyncAll cycle, alongside
// DiscoverDescendants, to register Claude workflow subagent transcripts
// (subagents/workflows/<runId>/agent-<id>.jsonl) and run journals
// (.../journal.jsonl) as path-encoded sidechain files (CF-533). The
// allow predicate is supplied by the engine and gates each file by its
// file_type against backend capability ("agent" → workflow_files,
// "workflow_journal" → workflow_journal); the provider calls it only
// after it has a candidate file, so non-workflow sessions never trigger
// a backend capability probe. Returns the count of newly-registered
// files (for logging). Claude scans the filesystem; Codex is a no-op
// (no Workflow-tool equivalent). Must be idempotent across calls.
DiscoverWorkflowFiles(reg WorkflowRegistrar, allow func(fileType string) bool) (int, error)
// AnnotateChunk is called for every chunk read from a tracked file,
// BEFORE upload. Providers attach provider-specific chunk metadata
// (codex_rollout, first_user_message, summary). The redact closure is
// nil-safe; providers must guard `if redact != nil { ... }` before
// applying. Engine inspects the returned AnnotationResult to flip its
// sentFirstUserMessage flag and dispatch any extracted summary links.
AnnotateChunk(c ChunkView, sentFirstUserMessage bool, redact func(string) string) AnnotationResult
// ScanSessions returns the user-initiated sessions discoverable on
// disk for this provider, sorted oldest first. Subagent rollouts and
// other non-user transcripts are filtered out.
ScanSessions() ([]SessionInfo, error)
// FindSessionByID resolves a full or partial session ID to its full
// ID and transcript path. For providers with a thread tree (Codex)
// this walks up to the root so the returned (id, path) refer to the
// top-most user session — callers that want the unwalked rollout use
// provider-specific methods.
FindSessionByID(partialID string) (id, transcriptPath string, err error)
// ExtractMetadata parses summary, first user message, and
// (Claude-only) summary links from in-memory transcript lines.
// Implementations cap the line count to bound cost.
ExtractMetadata(lines []string) SessionMetadata
// DefaultCWD returns the working directory to record alongside an
// upload for this transcript path. Claude derives from the path;
// Codex reads session_meta.cwd with a path-dir fallback.
DefaultCWD(transcriptPath string) string
// OnAlreadyRunning is invoked by maybeSpawnDaemon when a spawn is
// denied because a daemon for this externalID is already alive.
// Most providers no-op (hook deduplication is the normal path).
// OpenCode logs a warning because, for opencode, this state means a
// parallel process resumed the same session — an unsupported workflow.
OnAlreadyRunning(externalID string)
}
Provider abstracts per-tool local behavior. Adding a new provider means implementing this interface and registering the instance.
func Get ¶
Get returns the registered Provider for name. An empty name is an explicit error (ErrNoProvider) rather than a silent claude-code fallback (kata frm7).
func GetWithDir ¶ added in v0.17.1
GetWithDir returns a provider configured to install into / resolve against a non-default config dir (kata hpec). Only claude-code is wired this ticket; codex/opencode return an error until their fast-follow adds the override. dir == "" is equivalent to Get(name).
type RootTranscriptProvider ¶ added in v0.17.3
type RootTranscriptProvider interface {
RootTranscriptPath() string
}
RootTranscriptProvider exposes the session's root transcript path. Providers whose subagent files do NOT sit at SubagentsDir() (Cursor keeps them beside the transcript at filepath.Dir(transcript)/subagents, not under the Claude-shaped <session-id>/subagents) derive their subagents directory from this path. *sync.FileTracker satisfies it via RootTranscriptPath. The registrar passed to DiscoverDescendants is the *FileTracker, so Cursor type-asserts to this (plus WorkflowRegistrar) to reach what it needs.
type SessionInfo ¶
type SessionInfo struct {
SessionID string
TranscriptPath string
ProjectPath string
ModTime time.Time
SizeBytes int64
Summary string
FirstUserMessage string
}
SessionInfo is the cross-provider shape returned by Provider.ScanSessions and Provider.FindSessionByID. Concrete provider types may keep richer internal forms (e.g. CodexSessionInfo) and project to SessionInfo at the seams.
type SessionMetadata ¶
type SessionMetadata struct {
Summary string
FirstUserMessage string
SummaryLinks []SummaryLink
}
SessionMetadata is the parsed metadata for a transcript file or in-memory chunk. SummaryLinks are Claude-only and stay nil for other providers.
type SummaryLink ¶
SummaryLink describes a parent-session summary link extracted from a Claude transcript chunk. The engine HTTPs them after AnnotateChunk returns; the provider remains side-effect-free.
type TranscriptRegistrar ¶
type TranscriptRegistrar interface {
SetCodexRollout(*CodexRolloutMetadata)
}
TranscriptRegistrar is the minimal surface InitTranscript sees on the root transcript's TrackedFile. *sync.TrackedFile satisfies it structurally via SetCodexRollout. Lives here (not pkg/sync) to avoid an import cycle.
type WorkflowRegistrar ¶ added in v0.16.2
type WorkflowRegistrar interface {
SubagentsDir() string
RegisterSidechainFile(path, name, fileType string) bool
}
WorkflowRegistrar is the surface DiscoverWorkflowFiles uses to register Claude workflow subagent transcripts + run journals as path-encoded sidechain files. *sync.FileTracker satisfies it. SubagentsDir exposes the session's <session>/subagents directory (workflow runs live beneath subagents/workflows/<runId>/); RegisterSidechainFile tracks a file by its backend file_name (forward-slash path-encoded) and reports whether it was newly added (vs. an in-place path/type correction of an existing entry).