diff --git a/README.md b/README.md index 4de17a5..f7dee8e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Grok Build, Codex, Cursor, Merlin, corvid-agent, private runners). ```bash let where . # what agent stuff is here? let find worktrees --json # .claude/worktrees + ~/.codex + git + … -let find skills --json # Claude + Grok + Cursor + agent.3md planes +let find skills --json # Claude + Grok + Cursor + OpenAI + agent.3md planes let find agents --json # agent.3md + host agent dirs let find memory --json # memtrace / codex / gemini / .let (path-only) let find sessions --json # all hosts, path-only @@ -36,14 +36,14 @@ let web # local, read-only Fleet dashboard - **The let standard** — one closed kind set for every host; cards first, bodies on demand - **Worktrees** — git seed + Claude / Codex / Cursor / project / Grok DB, deduped by realpath -- **Skills** — Claude, Grok, Cursor, Codex, let catalogs + **agent.3md skill planes** +- **Skills** — Claude, Grok, Cursor, Codex, OpenAI, let catalogs + **agent.3md skill planes** - **Agents** — agent.3md / `*.3md` first-class + Claude/Codex/Cursor/Grok/Gemini/Kimi roots - **Commands** — Claude + Cursor slash-commands (project + user) - **Sessions / memory** — path-only federation (Claude, Grok, Codex, Cursor, Gemini, Kimi) - **Plugins / MCP / tasks** — Claude plugins & tasks, Cursor plans, MCP configs across hosts - **Workflows / superskills** — let-native + Grok workflow roots when present - **Show / open / route** — progressive bodies + agent.3md-first skill ranking -- **Hosts** — Claude, Codex, Cursor, Grok, Gemini, Kimi Code, agent.3md, let +- **Hosts** — Claude, Codex, Cursor, Grok, OpenAI, Gemini, Kimi Code, agent.3md, let - **Dogfood** — ships `agent.3md` and can find + show + route itself - **JSON-first** — stable envelopes for agents (`--json`) diff --git a/docs/dogfood.md b/docs/dogfood.md index 5262c89..ac39a42 100644 --- a/docs/dogfood.md +++ b/docs/dogfood.md @@ -29,7 +29,7 @@ let show skill --json # load body / tool template ## Multi-host on this machine ```bash -let doctor --json # roots for claude/codex/cursor/grok/gemini/kimi +let doctor --json # roots for claude/codex/cursor/grok/openai/gemini/kimi let find sessions --host kimi --scope user --json let find sessions --host gemini --scope user --json let find agents --json # agent3md + claude/codex + gemini diff --git a/docs/usage.md b/docs/usage.md index de8136c..8054f38 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -119,7 +119,7 @@ let find instructions --json Kinds: `instructions`, `skills`, `agents`, `commands`, `worktrees`, `sessions`, `tasks`, `memory`, `mcp`, `plugins`, `workflows`, `superskills`. -Hosts (`--host`): `claude`, `grok`, `codex`, `cursor`, `gemini`, `kimi`, +Hosts (`--host`): `claude`, `grok`, `codex`, `cursor`, `openai`, `gemini`, `kimi`, `agent3md`, `git`, `project`, `let`, … ### Host × kind matrix @@ -130,6 +130,7 @@ Hosts (`--host`): `claude`, `grok`, `codex`, `cursor`, `gemini`, `kimi`, | grok | worktrees.db | yes | bundled | — | path-only | memtrace | — | — | — | | codex | yes | optional | yes | — | path-only | sqlite/dir | yes | config path | — | | cursor | weak | yes | yes | yes | chats path | — | — | mcp.json | plans | +| openai | — | yes | — | — | — | — | — | — | — | | gemini | — | — | antigravity | — | history path | brain/knowledge | — | mcp_config | — | | kimi | — | — | config path | — | workspaces path | user-history | — | — | — | | agent3md | — | skill planes | agent.3md | — | — | — | — | — | — | diff --git a/specs/catalog/catalog.spec.md b/specs/catalog/catalog.spec.md index 06a7647..95a3ebf 100644 --- a/specs/catalog/catalog.spec.md +++ b/specs/catalog/catalog.spec.md @@ -1,6 +1,6 @@ --- module: catalog -version: 1 +version: 4 status: active files: - src/catalog/types.ts @@ -49,7 +49,7 @@ Context packs never include session paths or bodies. | Export | Description | |--------|-------------| -| `HostId` | Host attribution id (claude, grok, codex, cursor, gemini, kimi, git, project, corvid, let, agent3md, unknown). | +| `HostId` | Host attribution id (claude, grok, codex, cursor, openai, gemini, kimi, git, project, corvid, let, agent3md, unknown). | | `FindKind` | Closed catalog kind union for find/show. | | `FIND_KINDS` | Runtime list of all FindKind values. | | `FindScope` | project \| user \| all. | @@ -115,7 +115,7 @@ Context packs never include session paths or bodies. | `pathCardId` | Document caller-visible behavior and constraints. | | `inRepoWorktreeBases` | Document caller-visible behavior and constraints. | | `externalWorktreeRoots` | Document caller-visible behavior and constraints. | -| `skillRoots` | Document caller-visible behavior and constraints. | +| `skillRoots` | Fixed project and user skill catalog roots, including `.openai/skills`; user roots obey `include_user_skills`. | | `isDeniedBasename` | Document caller-visible behavior and constraints. | | `MAX_BODY_BYTES` | Document caller-visible behavior and constraints. | | `MAX_OPEN_PREVIEW_BYTES` | Document caller-visible behavior and constraints. | @@ -155,7 +155,7 @@ Context packs never include session paths or bodies. | Export | Description | |--------|-------------| -| `HostId` | claude, grok, codex, cursor, gemini, kimi, git, project, corvid, let, agent3md, unknown. | +| `HostId` | claude, grok, codex, cursor, openai, gemini, kimi, git, project, corvid, let, agent3md, unknown. | | `FindKind` | Closed catalog kinds (worktrees, skills, instructions, sessions, ...). | | `FindScope` | project \| user \| all. | | `CardScope` | project \| user \| global on individual cards. | @@ -255,6 +255,7 @@ Then body is undefined and payload.path_only is true | Version | Date | Changes | |---------|------|---------| +| 4 | 2026-08-01 | Add OpenAI skill-root discovery and attribution. | | 1 | 2026-07-30 | Federated find/where/context (PR1b). | | 2 | 2026-07-31 | Full host federation: memory/plugins/mcp/tasks/commands; agent.3md first-class. | | 3 | 2026-07-31 | Security: open path_only refusal; project session scope; partial body reads; underRepo bounds. | diff --git a/specs/doctor/doctor.spec.md b/specs/doctor/doctor.spec.md index 96655db..9a5e2d3 100644 --- a/specs/doctor/doctor.spec.md +++ b/specs/doctor/doctor.spec.md @@ -1,6 +1,6 @@ --- module: doctor -version: 1 +version: 2 status: active files: - src/doctor.ts @@ -32,6 +32,7 @@ defaults. Used by `let doctor --json`. 2. Report always includes a git check and bun check. 3. Config summary exposes allow_shell_exec and default_limit. 4. Codex worktrees root is noted as shallow-scan only when present. +5. OpenAI home and skills roots are reported when skill discovery supports OpenAI. ## Behavioral Examples @@ -57,4 +58,5 @@ Then checks include id "git" with ok true and id "bun" with ok true | Version | Date | Changes | |---------|------|---------| +| 2 | 2026-08-01 | Report OpenAI home and skills roots. | | 1 | 2026-07-30 | Initial doctor contract. | diff --git a/specs/paths/paths.spec.md b/specs/paths/paths.spec.md index c7311ad..1740c36 100644 --- a/specs/paths/paths.spec.md +++ b/specs/paths/paths.spec.md @@ -1,6 +1,6 @@ --- module: paths -version: 1 +version: 2 status: active files: - src/paths.ts @@ -12,8 +12,8 @@ depends_on: [] ## Purpose -Host and project path helpers for Claude, Grok, Codex, Cursor, and let directories, -plus Claude project path encoding. +Host and project path helpers for Claude, Grok, Codex, Cursor, Gemini, Kimi, OpenAI, and let +directories, plus Claude project path encoding. ## Public API @@ -26,6 +26,7 @@ plus Claude project path encoding. | `grokHome` | `~/.grok`. | | `codexHome` | `~/.codex`. | | `cursorHome` | `~/.cursor`. | +| `openaiHome` | `~/.openai`. | | `geminiHome` | `~/.gemini`. | | `kimiHome` | `~/.kimi-code`. | | `projectClaudeDir` | `/.claude`. | @@ -63,4 +64,5 @@ Then result is -Users-leif-Development--CorvidLabs-quill | Version | Date | Changes | |---------|------|---------| +| 2 | 2026-08-01 | Add the OpenAI home helper contract. | | 1 | 2026-07-30 | Initial path helpers. | diff --git a/src/catalog/scan-policy.ts b/src/catalog/scan-policy.ts index 14e70e4..742b6f1 100644 --- a/src/catalog/scan-policy.ts +++ b/src/catalog/scan-policy.ts @@ -10,6 +10,7 @@ import { cursorHome, grokHome, homeDir, + openaiHome, } from "../paths.ts"; export type ScanPolicy = { @@ -88,6 +89,7 @@ export function skillRoots(repoRoot: string | null): { join(repoRoot, ".grok", "skills"), join(repoRoot, ".cursor", "skills"), join(repoRoot, ".cursor", "skills-cursor"), + join(repoRoot, ".openai", "skills"), join(repoRoot, ".let", "skills"), join(repoRoot, "skills"), join(repoRoot, ".agents", "skills"), @@ -99,6 +101,7 @@ export function skillRoots(repoRoot: string | null): { join(grokHome(), "skills"), join(cursorHome(), "skills-cursor"), join(cursorHome(), "skills"), + join(openaiHome(), "skills"), join(codexHome(), "skills"), join(homeDir(), ".let", "skills"), ]; diff --git a/src/catalog/skills.ts b/src/catalog/skills.ts index dc6b9a1..ade1dcd 100644 --- a/src/catalog/skills.ts +++ b/src/catalog/skills.ts @@ -12,6 +12,7 @@ import { pathExists, readTextFile, } from "../fs-scan.ts"; +import { openaiHome } from "../paths.ts"; import { skillId } from "./ids.ts"; import { skillRoots } from "./scan-policy.ts"; import type { HostId, IndexCard } from "./types.ts"; @@ -26,6 +27,12 @@ function hostForSkillRoot(root: string, ctx: ScanContext): HostId { if (root.includes("/.cursor/") || root.includes("skills-cursor")) { return "cursor"; } + if ( + root === join(openaiHome(), "skills") || + (ctx.repoRoot !== null && root === join(ctx.repoRoot, ".openai", "skills")) + ) { + return "openai"; + } if (root.includes("/.codex/")) { return "codex"; } diff --git a/src/catalog/types.ts b/src/catalog/types.ts index eb59fec..bd7302d 100644 --- a/src/catalog/types.ts +++ b/src/catalog/types.ts @@ -7,6 +7,7 @@ export type HostId = | "grok" | "codex" | "cursor" + | "openai" | "gemini" | "kimi" | "git" diff --git a/src/doctor.ts b/src/doctor.ts index 93279cf..58e3009 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -13,6 +13,7 @@ import { grokHome, homeDir, kimiHome, + openaiHome, projectClaudeDir, } from "./paths.ts"; @@ -104,6 +105,8 @@ export function runDoctor(cwd: string = process.cwd()): DoctorReport { "cursor.chats": rootEntry(join(cursorHome(), "chats"), "path-only"), "cursor.plans": rootEntry(join(cursorHome(), "plans"), "tasks kind"), "cursor.worktrees": rootEntry(join(cursorHome(), "worktrees")), + "openai.home": rootEntry(openaiHome()), + "openai.skills": rootEntry(join(openaiHome(), "skills")), "gemini.home": rootEntry( geminiHome(), "GEMINI.md + history + projects.json", diff --git a/src/index.ts b/src/index.ts index 40e87a2..233c8d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -94,6 +94,7 @@ export { grokHome, homeDir, kimiHome, + openaiHome, projectClaudeDir, projectGeminiDir, projectLetDir, diff --git a/src/paths.ts b/src/paths.ts index d572444..38367bc 100644 --- a/src/paths.ts +++ b/src/paths.ts @@ -26,6 +26,11 @@ export function cursorHome(): string { return join(homeDir(), ".cursor"); } +/** OpenAI agent/CLI home. */ +export function openaiHome(): string { + return join(homeDir(), ".openai"); +} + /** Google Gemini CLI / Antigravity home. */ export function geminiHome(): string { return join(homeDir(), ".gemini"); diff --git a/test/doctor.test.ts b/test/doctor.test.ts index fa07025..cde17e6 100644 --- a/test/doctor.test.ts +++ b/test/doctor.test.ts @@ -7,6 +7,7 @@ describe("runDoctor", () => { expect(report.version).toBeTruthy(); expect(report.checks.length).toBeGreaterThan(0); expect(report.roots["claude.home"]).toBeDefined(); + expect(report.roots["openai.skills"]).toBeDefined(); expect(report.roots["project.claude.worktrees"]).toBeDefined(); const git = report.checks.find((c) => c.id === "git"); expect(git).toBeDefined(); diff --git a/test/find.test.ts b/test/find.test.ts index 9cb09e1..c85fc6a 100644 --- a/test/find.test.ts +++ b/test/find.test.ts @@ -1,8 +1,12 @@ import { describe, expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { buildContext } from "../src/catalog/context.ts"; import { buildScanContext } from "../src/catalog/context-builder.ts"; import { findAssets } from "../src/catalog/find.ts"; import { whereAmI } from "../src/catalog/where.ts"; +import { DEFAULT_CONFIG } from "../src/config.ts"; describe("find", () => { test("find instructions in let repo", async () => { @@ -23,6 +27,58 @@ describe("find", () => { expect(r.items.length).toBeLessThanOrEqual(ctx.limit); }); + test("find skills discovers project openai skill roots", async () => { + const root = mkdtempSync(join(tmpdir(), "let-openai-")); + Bun.spawnSync(["git", "init"], { cwd: root }); + const skillDir = join(root, ".openai", "skills", "agent-coordination"); + mkdirSync(skillDir, { recursive: true }); + writeFileSync( + join(skillDir, "SKILL.md"), + "---\nname: agent-coordination\ndescription: Coordinate agents\n---\n# Coordination\n", + ); + const ctx = buildScanContext({ + cwd: root, + scope: "project", + config: { + ...DEFAULT_CONFIG, + find: { ...DEFAULT_CONFIG.find, include_user_skills: false }, + }, + }); + const r = await findAssets("skills", ctx, { host: "openai" }); + expect(r.items).toHaveLength(1); + expect(r.items[0]?.host).toBe("openai"); + expect(r.items[0]?.name).toBe("agent-coordination"); + expect(realpathSync(r.items[0]?.path ?? "")).toBe( + realpathSync(join(skillDir, "SKILL.md")), + ); + }); + + test("find skills keeps generic project roots below .openai attributed to project", async () => { + const parent = mkdtempSync(join(tmpdir(), "let-openai-parent-")); + const root = join(parent, ".openai", "worktrees", "repo"); + mkdirSync(root, { recursive: true }); + Bun.spawnSync(["git", "init"], { cwd: root }); + const skillDir = join(root, "skills", "generic-skill"); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, "SKILL.md"), "# Generic skill\n"); + + const ctx = buildScanContext({ + cwd: root, + scope: "project", + config: { + ...DEFAULT_CONFIG, + find: { ...DEFAULT_CONFIG.find, include_user_skills: false }, + }, + }); + const r = await findAssets("skills", ctx, { host: "project" }); + const genericSkill = r.items.find( + (item) => + realpathSync(item.path) === realpathSync(join(skillDir, "SKILL.md")), + ); + + expect(genericSkill?.host).toBe("project"); + }); + test("find worktrees dedupes paths", async () => { const ctx = buildScanContext({ cwd: process.cwd() }); const r = await findAssets("worktrees", ctx);