Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added

- Cortex Code (Snowflake) transcript provider (`cortexCode`)
- Discovers sessions from `~/.snowflake/cortex/conversations/*.json`
- Filters by `working_directory` to scope to workspace
- Extracts connection name, message count, tool call count, and session type as metadata
- Status detection based on `last_updated` recency and active tool calls
- Header caching by mtime to avoid re-reading unchanged files
- Watch support via shared `createProviderWatch`
- Public entry point: `@agentprobe/core/providers/cortex-code`
- 41 new tests across schemas, discovery, transcripts, and provider integration

## [0.1.2] - 2026-03-06

### Fixed
Expand Down
Binary file modified README.md
Binary file not shown.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@
"import": "./dist/providers/opencode/index.js",
"require": "./dist/providers/opencode/index.cjs",
"default": "./dist/providers/opencode/index.js"
},
"./providers/cortex-code": {
"types": "./dist/providers/cortex-code/index.d.ts",
"import": "./dist/providers/cortex-code/index.js",
"require": "./dist/providers/cortex-code/index.cjs",
"default": "./dist/providers/cortex-code/index.js"
}
},
"typesVersions": {
Expand All @@ -71,6 +77,9 @@
],
"providers/opencode": [
"dist/providers/opencode/index.d.ts"
],
"providers/cortex-code": [
"dist/providers/cortex-code/index.d.ts"
]
}
},
Expand Down
1 change: 1 addition & 0 deletions src/core/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const PROVIDER_KINDS = {
codex: "codex",
claudeCode: "claude-code",
openCode: "opencode",
cortexCode: "cortex-code",
} as const;
export type ProviderKind = (typeof PROVIDER_KINDS)[keyof typeof PROVIDER_KINDS];

Expand Down
13 changes: 12 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { createCompositeProvider } from "./core/composite";
import { claudeCode } from "./providers/claude-code";
import { codex } from "./providers/codex";
import { cortexCode } from "./providers/cortex-code";
import { cursor } from "./providers/cursor";
import { openCode } from "./providers/opencode";

Expand Down Expand Up @@ -74,6 +75,10 @@ export {
type CodexOptions,
codex,
} from "./providers/codex";
export {
type CortexCodeOptions,
cortexCode,
} from "./providers/cortex-code";
export {
type CursorOptions,
cursor,
Expand All @@ -88,7 +93,13 @@ export interface CreateObserverOptions extends Omit<ObserverOptions, "provider">
}

export function createObserver(options: CreateObserverOptions): Observer {
const providers = options.providers ?? [cursor(), claudeCode(), codex(), openCode()];
const providers = options.providers ?? [
cursor(),
claudeCode(),
codex(),
openCode(),
cortexCode(),
];
const provider = providers.length === 1 ? providers[0] : createCompositeProvider(providers);

return createCoreObserver({
Expand Down
7 changes: 7 additions & 0 deletions src/providers/cortex-code/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const CORTEX_CODE_SOURCE_KIND = "cortex-code-sessions";
export const CORTEX_CODE_WATCH_DEBOUNCE_MS = 150;
export const CORTEX_CODE_RUNNING_WINDOW_MS = 3_000;
export const CORTEX_CODE_IDLE_WINDOW_MS = 60_000;
export const MAX_DISCOVERED_SESSION_FILES = 50;
export const CORTEX_CODE_HOME_SUBPATH = ".snowflake/cortex";
export const AGENT_NAME_PREFIX_LENGTH = 6;
140 changes: 140 additions & 0 deletions src/providers/cortex-code/discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { homedir } from "node:os";
import path from "node:path";
import {
collectJsonlFiles,
dedupePaths,
directoryExists,
normalizeWorkspacePath,
} from "@/providers/shared/discovery";
import { readSourceFile } from "@/providers/shared/providers";
import { CORTEX_CODE_HOME_SUBPATH, MAX_DISCOVERED_SESSION_FILES } from "./constants";

export interface SessionDiscoveryOptions {
workspacePaths: string[];
cortexHomePath?: string;
maxFiles?: number;
}

function resolveCortexHome(options: SessionDiscoveryOptions): string {
return options.cortexHomePath ?? path.join(homedir(), CORTEX_CODE_HOME_SUBPATH);
}

export function resolveConversationsDirectory(options: SessionDiscoveryOptions): string {
return path.join(resolveCortexHome(options), "conversations");
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

function matchesWorkspace(cwd: string, normalizedPaths: readonly string[]): boolean {
const normalizedCwd = normalizeWorkspacePath(cwd);
return normalizedPaths.some((wp) => normalizedCwd === wp || normalizedCwd.startsWith(`${wp}/`));
}

interface ConversationHeader {
mtimeMs: number;
workingDirectory: string;
sessionId: string;
}

const headerCache = new Map<string, ConversationHeader>();

function parseConversationHeader(
contents: string,
mtimeMs: number,
): ConversationHeader | undefined {
try {
const parsed: unknown = JSON.parse(contents);
if (!isRecord(parsed)) {
return undefined;
}
const workingDirectory = parsed.working_directory;
const sessionId = typeof parsed.session_id === "string" ? parsed.session_id : "";
if (typeof workingDirectory !== "string" || workingDirectory.length === 0) {
return undefined;
}
return { mtimeMs, workingDirectory, sessionId };
} catch {
return undefined;
}
}

async function getConversationHeader(
filePath: string,
mtimeMs: number,
): Promise<ConversationHeader | undefined> {
const cached = headerCache.get(filePath);
if (cached && cached.mtimeMs === mtimeMs) {
return cached;
}

const contents = await readSourceFile(filePath);
if (contents === null) {
headerCache.delete(filePath);
return undefined;
}

const header = parseConversationHeader(contents, mtimeMs);
if (header) {
headerCache.set(filePath, header);
} else {
headerCache.delete(filePath);
}
return header;
}

export async function resolveSessionSourcePaths(
options: SessionDiscoveryOptions,
): Promise<string[]> {
const maxFiles = options.maxFiles ?? MAX_DISCOVERED_SESSION_FILES;
const conversationsDir = resolveConversationsDirectory(options);

if (!directoryExists(conversationsDir)) {
return [];
}

const normalizedPaths = options.workspacePaths
.map(normalizeWorkspacePath)
.filter((p) => p.length > 0);

if (normalizedPaths.length === 0) {
return [];
}

const allFiles = collectJsonlFiles([conversationsDir], {
recursive: false,
extension: ".json",
});

const matching: { path: string; mtimeMs: number }[] = [];

for (const file of allFiles) {
const header = await getConversationHeader(file.path, file.mtimeMs);
if (!header) {
continue;
}
if (matchesWorkspace(header.workingDirectory, normalizedPaths)) {
matching.push({ path: file.path, mtimeMs: file.mtimeMs });
}
}

return matching
.sort((left, right) => right.mtimeMs - left.mtimeMs || left.path.localeCompare(right.path))
.slice(0, maxFiles)
.map((entry) => entry.path);
}

export function listSessionFileNames(options: SessionDiscoveryOptions): string[] {
const conversationsDir = resolveConversationsDirectory(options);

if (!directoryExists(conversationsDir)) {
return [];
}

return dedupePaths(
collectJsonlFiles([conversationsDir], { recursive: false, extension: ".json" }).map(
(f) => f.path,
),
).sort();
}
29 changes: 29 additions & 0 deletions src/providers/cortex-code/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export {
listSessionFileNames,
resolveConversationsDirectory,
resolveSessionSourcePaths,
type SessionDiscoveryOptions,
} from "./discovery";
export {
type CortexCodeOptions,
cortexCode,
} from "./provider";
export {
type ContentBlock,
type CortexCodeConversation,
extractUserTaskSummary,
type HistoryMessage,
parseConversation,
} from "./schemas";
export {
type CortexCodeTranscriptSource,
type CortexCodeTranscriptSourceOptions,
type CortexCodeTranscriptSourceResult,
createCortexCodeTranscriptSource,
} from "./transcripts";
export {
CORTEX_CODE_WATCH_DEBOUNCE_MS,
type CortexCodeWatch,
type CortexCodeWatchOptions,
createCortexCodeWatch,
} from "./watch";
Loading