Skip to content

Commit ff48461

Browse files
authored
Merge pull request #11 from lewisnsmith/claude/create-claude-code-package-XhmgM
feat: monorepo restructure + @flight/claude-code shim package
2 parents 75332b2 + 2929f36 commit ff48461

101 files changed

Lines changed: 1617 additions & 620 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ jobs:
3030
- name: Type check
3131
run: npm run typecheck
3232

33-
- name: Test
34-
run: npm test
35-
3633
- name: Build
3734
run: npm run build
35+
36+
- name: Test
37+
run: npm test

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ with FlightClient(
109109

110110
### Claude Code Integration
111111

112+
> **Quickest path:** `npx @flight/claude-code@latest init` — no global install required.
113+
112114
```bash
113115
# Install from source
114116
git clone https://github.com/lewisnsmith/flight.git

package-lock.json

Lines changed: 1257 additions & 560 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,21 @@
11
{
2-
"name": "flight-proxy",
3-
"version": "1.4.0",
4-
"description": "Agent observability platform — structured tracing, audit, and replay for AI agent systems",
2+
"name": "flight-monorepo",
3+
"private": true,
4+
"version": "0.0.0",
5+
"description": "Flight monorepo — agent observability platform",
56
"type": "module",
6-
"main": "dist/index.js",
7-
"types": "dist/index.d.ts",
8-
"bin": {
9-
"flight": "dist/cli.js"
10-
},
7+
"workspaces": ["packages/*"],
118
"scripts": {
12-
"build": "tsup",
13-
"dev": "tsup --watch",
14-
"test": "vitest run",
15-
"test:watch": "vitest",
16-
"lint": "eslint src/",
17-
"typecheck": "tsc --noEmit && tsc --project tsconfig.test.json",
18-
"bench": "npx tsx bench/throughput.ts",
19-
"check": "npm run lint && npm run typecheck && npm run test",
20-
"prepublishOnly": "npm run build"
9+
"build": "npm run build --workspace flight-proxy",
10+
"test": "npm test --workspaces --if-present",
11+
"lint": "npm run lint --workspace flight-proxy",
12+
"typecheck": "npm run typecheck --workspace flight-proxy",
13+
"check": "npm run check --workspaces --if-present",
14+
"bench": "npm run bench --workspace flight-proxy"
2115
},
22-
"keywords": [
23-
"agent",
24-
"observability",
25-
"tracing",
26-
"audit",
27-
"multi-agent",
28-
"mcp",
29-
"ai",
30-
"tool-calling",
31-
"hallucination",
32-
"flight-recorder"
33-
],
3416
"author": "Lewis Smith",
3517
"license": "MIT",
3618
"engines": {
3719
"node": ">=20.11.0"
38-
},
39-
"files": [
40-
"dist"
41-
],
42-
"dependencies": {
43-
"@inquirer/prompts": "^8.3.2",
44-
"better-sqlite3": "^12.8.0",
45-
"blessed": "^0.1.81",
46-
"commander": "^14.0.3"
47-
},
48-
"devDependencies": {
49-
"@eslint/js": "^10.0.1",
50-
"@types/better-sqlite3": "^7.6.13",
51-
"@types/blessed": "^0.1.27",
52-
"@types/node": "^25.3.3",
53-
"eslint": "^10.0.2",
54-
"tsup": "^8.5.1",
55-
"tsx": "^4.21.0",
56-
"typescript": "^5.9.3",
57-
"typescript-eslint": "^8.56.1",
58-
"vitest": "^4.0.18"
5920
}
6021
}

packages/claude-code/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# @flight/claude-code
2+
3+
## What this does
4+
5+
Runs the Flight setup wizard so Claude Code records every tool call to `~/.flight/logs/`. Flight is an agent observability platform that provides structured tracing, audit, and replay for AI agent systems. Running `npx @flight/claude-code@latest init` installs the necessary Claude Code hooks (SessionStart, SessionEnd, PostToolUse) and optionally sets up slash commands and MCP server proxying — no global CLI install required.
6+
7+
## Usage
8+
9+
```
10+
npx @flight/claude-code@latest init
11+
```
12+
13+
## Flags
14+
15+
- `--hooks` / `--no-hooks` — Install or skip Claude Code hooks
16+
- `--proxy` / `--no-proxy` — Wrap or skip MCP server proxying
17+
- `--pd` / `--no-pd` — Enable or disable progressive disclosure
18+
- `--slash-commands` / `--no-slash-commands` — Install or skip /flight slash commands
19+
- `--banner` / `--no-banner` — Show or skip the setup banner
20+
- `--remove` — Uninstall Flight integration
21+
- `--help`, `-h` — Show usage
22+
23+
## Versioning
24+
25+
0.x.y — breaking changes allowed until 1.0.0.
26+
27+
## More
28+
29+
See the full Flight documentation at https://github.com/lewisnsmith/flight.

packages/claude-code/bin/init.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env node
2+
3+
import { runSetupWizard, runRemove } from "flight-proxy";
4+
5+
const USAGE = `Usage: npx @flight/claude-code init [options]
6+
7+
Sets up Flight (agent observability) for Claude Code: installs hooks,
8+
slash commands, and optionally wraps MCP servers.
9+
10+
Subcommands:
11+
init Run the Flight setup wizard
12+
13+
Options:
14+
--hooks, --no-hooks Install/skip Claude Code hooks
15+
--proxy, --no-proxy Wrap/skip MCP server proxying
16+
--pd, --no-pd Enable/disable progressive disclosure
17+
--slash-commands, --no-slash-commands Install/skip /flight slash commands
18+
--banner, --no-banner Show/skip the setup banner
19+
--remove Uninstall Flight integration
20+
--help, -h Show this help
21+
22+
See https://github.com/lewisnsmith/flight for full docs.
23+
`;
24+
25+
/**
26+
* Parse process.argv into subcommand + overrides object.
27+
* Returns { subcommand, overrides, remove, help }.
28+
*/
29+
function parseArgs(argv) {
30+
const args = argv.slice(2);
31+
let subcommand = null;
32+
let remove = false;
33+
let help = false;
34+
const overrides = {};
35+
36+
let i = 0;
37+
while (i < args.length) {
38+
const arg = args[i];
39+
40+
if (arg === "--help" || arg === "-h") {
41+
help = true;
42+
} else if (arg === "--remove") {
43+
remove = true;
44+
} else if (arg === "--hooks") {
45+
overrides.hooks = true;
46+
} else if (arg === "--no-hooks") {
47+
overrides.hooks = false;
48+
} else if (arg === "--proxy") {
49+
overrides.proxy = true;
50+
} else if (arg === "--no-proxy") {
51+
overrides.proxy = false;
52+
} else if (arg === "--pd") {
53+
overrides.pd = true;
54+
} else if (arg === "--no-pd") {
55+
overrides.pd = false;
56+
} else if (arg === "--slash-commands") {
57+
overrides.slashCommands = true;
58+
} else if (arg === "--no-slash-commands") {
59+
overrides.slashCommands = false;
60+
} else if (arg === "--banner") {
61+
overrides.banner = true;
62+
} else if (arg === "--no-banner") {
63+
overrides.banner = false;
64+
} else if (!arg.startsWith("-")) {
65+
subcommand = arg;
66+
} else {
67+
process.stderr.write(`Unknown option: ${arg}\n\n${USAGE}`);
68+
process.exit(1);
69+
}
70+
i++;
71+
}
72+
73+
return { subcommand, overrides, remove, help };
74+
}
75+
76+
async function main() {
77+
const { subcommand, overrides, remove, help } = parseArgs(process.argv);
78+
79+
if (help) {
80+
process.stdout.write(USAGE);
81+
process.exit(0);
82+
}
83+
84+
if (subcommand === null) {
85+
process.stderr.write(`Error: a subcommand is required.\n\n${USAGE}`);
86+
process.exit(1);
87+
}
88+
89+
if (subcommand !== "init") {
90+
process.stderr.write(`Unknown subcommand: ${subcommand}\n\n${USAGE}`);
91+
process.exit(1);
92+
}
93+
94+
// subcommand === "init"
95+
if (remove) {
96+
await runRemove();
97+
return;
98+
}
99+
100+
await runSetupWizard(overrides);
101+
}
102+
103+
main().catch((err) => {
104+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
105+
process.exit(1);
106+
});

packages/claude-code/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@flight/claude-code",
3+
"version": "0.1.0",
4+
"description": "One-command Flight setup for Claude Code",
5+
"type": "module",
6+
"bin": {
7+
"flight-claude-code": "bin/init.js"
8+
},
9+
"scripts": {
10+
"test": "vitest run",
11+
"check": "vitest run"
12+
},
13+
"files": ["bin", "README.md"],
14+
"engines": { "node": ">=20" },
15+
"dependencies": {
16+
"flight-proxy": "^1.4.0"
17+
},
18+
"devDependencies": {
19+
"vitest": "^4.0.18"
20+
}
21+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, it, expect } from "vitest";
2+
import { spawnSync, type SpawnSyncReturns } from "node:child_process";
3+
import { mkdtempSync, readFileSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
4+
import { tmpdir } from "node:os";
5+
import { join, resolve } from "node:path";
6+
7+
const BIN = resolve(import.meta.dirname, "../bin/init.js");
8+
9+
function run(args: string[], env?: Record<string, string>): SpawnSyncReturns<string> {
10+
return spawnSync(process.execPath, [BIN, ...args], {
11+
encoding: "utf-8",
12+
timeout: 30_000,
13+
stdio: ["ignore", "pipe", "pipe"],
14+
env: { ...process.env, ...env },
15+
});
16+
}
17+
18+
describe("@flight/claude-code bin/init.js", () => {
19+
it("--help exits 0 and prints usage", () => {
20+
const result = run(["--help"]);
21+
expect(result.status).toBe(0);
22+
expect(result.stdout).toContain("Usage: npx @flight/claude-code init");
23+
});
24+
25+
it("-h exits 0 and prints usage", () => {
26+
const result = run(["-h"]);
27+
expect(result.status).toBe(0);
28+
expect(result.stdout).toContain("Usage: npx @flight/claude-code init");
29+
});
30+
31+
it("unknown subcommand exits non-zero and prints usage to stderr", () => {
32+
const result = run(["foo"]);
33+
expect(result.status).not.toBe(0);
34+
expect(result.stderr).toContain("Usage:");
35+
});
36+
37+
it("no subcommand exits non-zero and prints usage to stderr", () => {
38+
const result = run([]);
39+
expect(result.status).not.toBe(0);
40+
expect(result.stderr).toContain("Usage:");
41+
});
42+
43+
it("idempotency: running init twice produces exactly one set of hooks in settings.json", () => {
44+
// Check that the flight CLI is available (built) before running this test
45+
const flightCheck = spawnSync(process.execPath, [BIN, "--help"], {
46+
encoding: "utf-8",
47+
timeout: 5_000,
48+
});
49+
if (flightCheck.error) {
50+
console.log("Skipping idempotency test: bin/init.js not runnable:", flightCheck.error.message);
51+
return;
52+
}
53+
54+
const tmpHome = mkdtempSync(join(tmpdir(), "flight-idem-"));
55+
try {
56+
const claudeDir = join(tmpHome, ".claude");
57+
mkdirSync(claudeDir, { recursive: true });
58+
writeFileSync(join(claudeDir, "settings.json"), JSON.stringify({}));
59+
60+
const flags = ["init", "--hooks", "--no-proxy", "--slash-commands", "--no-pd", "--no-banner"];
61+
const env = { HOME: tmpHome };
62+
63+
// First run
64+
const first = run(flags, env);
65+
if (first.status !== 0) {
66+
console.log("Skipping idempotency test: first run failed:", first.stderr);
67+
return;
68+
}
69+
70+
// Second run (should be idempotent)
71+
const second = run(flags, env);
72+
if (second.status !== 0) {
73+
console.log("Skipping idempotency test: second run failed:", second.stderr);
74+
return;
75+
}
76+
77+
const settingsRaw = readFileSync(join(claudeDir, "settings.json"), "utf-8");
78+
const settings = JSON.parse(settingsRaw) as {
79+
hooks?: Record<string, Array<{ hooks: Array<{ command: string }> }>>;
80+
};
81+
82+
// Each hook event should have exactly one matcher with a flight command
83+
const hookEvents = ["SessionStart", "SessionEnd", "PostToolUse", "UserPromptSubmit"];
84+
for (const event of hookEvents) {
85+
const matchers = settings.hooks?.[event];
86+
expect(matchers, `${event} hooks should exist`).toBeDefined();
87+
// Collect all flight-related hook entries
88+
const flightMatchers = (matchers ?? []).filter((m) =>
89+
m.hooks.some((h) => h.command.includes("flight hook"))
90+
);
91+
expect(
92+
flightMatchers.length,
93+
`${event} should have exactly one flight hook matcher, got ${flightMatchers.length}`
94+
).toBe(1);
95+
}
96+
97+
// SessionStart should contain "flight hook session-start"
98+
const sessionStartCmd = settings.hooks?.["SessionStart"]?.[0]?.hooks?.[0]?.command ?? "";
99+
expect(sessionStartCmd).toContain("flight hook session-start");
100+
101+
// SessionEnd should contain "flight hook session-end"
102+
const sessionEndCmd = settings.hooks?.["SessionEnd"]?.[0]?.hooks?.[0]?.command ?? "";
103+
expect(sessionEndCmd).toContain("flight hook session-end");
104+
105+
// PostToolUse should contain "flight hook post-tool-use"
106+
const postToolCmd = settings.hooks?.["PostToolUse"]?.[0]?.hooks?.[0]?.command ?? "";
107+
expect(postToolCmd).toContain("flight hook post-tool-use");
108+
} finally {
109+
rmSync(tmpHome, { recursive: true, force: true });
110+
}
111+
});
112+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
export default defineConfig({
4+
test: {
5+
globals: true,
6+
pool: "forks",
7+
fileParallelism: false,
8+
testTimeout: 30000,
9+
},
10+
});

0 commit comments

Comments
 (0)