Skip to content

Commit f2430e9

Browse files
committed
Add cli package to monorepo
Add codeforge-cli v0.1.0 (Bun/TypeScript) — a CLI for CodeForge development workflows including session search, plan management, and task tracking. The docs package was already tracked from the previous repository structure.
1 parent 6f9de02 commit f2430e9

45 files changed

Lines changed: 3864 additions & 0 deletions

Some content is hidden

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

cli/bun.lock

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

cli/package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "codeforge-cli",
3+
"version": "0.1.0",
4+
"description": "CLI for CodeForge development workflows",
5+
"type": "module",
6+
"bin": {
7+
"codeforge": "./dist/codeforge.js"
8+
},
9+
"scripts": {
10+
"build": "bun build src/index.ts --outdir dist --target bun",
11+
"dev": "bun run src/index.ts",
12+
"test": "bun test"
13+
},
14+
"dependencies": {
15+
"commander": "^13.0.0",
16+
"chalk": "^5.4.0",
17+
"fast-glob": "^3.3.0"
18+
},
19+
"devDependencies": {
20+
"@types/bun": "^1.3.10",
21+
"@types/node": "^22.0.0",
22+
"typescript": "^5.7.0"
23+
},
24+
"engines": {
25+
"node": ">=18.0.0"
26+
},
27+
"license": "GPL-3.0",
28+
"author": "AnExiledDev"
29+
}

cli/src/commands/plan/search.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import chalk from "chalk";
2+
import type { Command } from "commander";
3+
import { loadPlans } from "../../loaders/plan-loader.js";
4+
import {
5+
formatPlanJson,
6+
formatPlanText,
7+
type PlanSearchResult,
8+
} from "../../output/plan-text.js";
9+
import { evaluate, parse } from "../../search/query-parser.js";
10+
11+
interface PlanSearchOptions {
12+
limit: string;
13+
format: string;
14+
color?: boolean;
15+
fullText?: boolean;
16+
}
17+
18+
function extractContextLines(content: string, query: string): string[] {
19+
const contentLines = content.split("\n");
20+
// Extract individual terms from the query (simple word extraction)
21+
const terms = query
22+
.replace(/\b(AND|OR|NOT)\b/gi, "")
23+
.replace(/[()]/g, "")
24+
.split(/\s+/)
25+
.filter((t) => t.length > 0)
26+
.map((t) => t.replace(/^["']|["']$/g, "").toLowerCase());
27+
28+
if (terms.length === 0) return [];
29+
30+
const matchingIndices = new Set<number>();
31+
32+
for (let i = 0; i < contentLines.length; i++) {
33+
const lower = contentLines[i].toLowerCase();
34+
for (const term of terms) {
35+
if (lower.includes(term)) {
36+
matchingIndices.add(i);
37+
break;
38+
}
39+
}
40+
}
41+
42+
// Add context lines (+/- 1 line)
43+
const contextIndices = new Set<number>();
44+
for (const idx of matchingIndices) {
45+
if (idx > 0) contextIndices.add(idx - 1);
46+
contextIndices.add(idx);
47+
if (idx < contentLines.length - 1) contextIndices.add(idx + 1);
48+
}
49+
50+
// Sort and deduplicate, cap at 5
51+
const sorted = [...contextIndices].sort((a, b) => a - b).slice(0, 5);
52+
return sorted.map((i) => contentLines[i]);
53+
}
54+
55+
export function registerPlanSearchCommand(parent: Command): void {
56+
parent
57+
.command("search")
58+
.description("Search across plan files")
59+
.argument("[query]", "Search query (supports AND, OR, NOT, quotes)")
60+
.option("-n, --limit <count>", "Maximum number of results", "20")
61+
.option("-f, --format <format>", "Output format: text|json", "text")
62+
.option("--no-color", "Disable colored output")
63+
.option("--full-text", "Disable content truncation")
64+
.action(async (query: string | undefined, options: PlanSearchOptions) => {
65+
try {
66+
if (!options.color) {
67+
chalk.level = 0;
68+
}
69+
70+
const plans = await loadPlans();
71+
72+
let results: PlanSearchResult[];
73+
74+
if (query) {
75+
const queryNode = parse(query);
76+
results = [];
77+
for (const plan of plans) {
78+
if (evaluate(queryNode, plan.content)) {
79+
const matchingLines = extractContextLines(plan.content, query);
80+
results.push({ plan, matchingLines });
81+
}
82+
}
83+
} else {
84+
results = plans.map((plan) => ({ plan }));
85+
}
86+
87+
// Apply limit
88+
const limit = parseInt(options.limit, 10);
89+
results = results.slice(0, limit);
90+
91+
if (options.format === "json") {
92+
console.log(formatPlanJson(results));
93+
} else {
94+
console.log(
95+
formatPlanText(results, {
96+
noColor: !options.color,
97+
fullText: options.fullText,
98+
}),
99+
);
100+
}
101+
} catch (err) {
102+
const message = err instanceof Error ? err.message : String(err);
103+
console.error(`Error: ${message}`);
104+
process.exit(1);
105+
}
106+
});
107+
}

cli/src/commands/session/list.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import chalk from "chalk";
2+
import type { Command } from "commander";
3+
import { loadHistory } from "../../loaders/history-loader.js";
4+
import { extractSessionMeta } from "../../loaders/session-meta.js";
5+
import {
6+
formatSessionListJson,
7+
formatSessionListText,
8+
type SessionListEntry,
9+
} from "../../output/session-list.js";
10+
import { discoverSessionFiles } from "../../utils/glob.js";
11+
import { parseRelativeTime, parseTime } from "../../utils/time.js";
12+
13+
interface ListCommandOptions {
14+
project?: string;
15+
since?: string;
16+
after?: string;
17+
before?: string;
18+
limit: string;
19+
format: string;
20+
color?: boolean;
21+
}
22+
23+
export function registerListCommand(parent: Command): void {
24+
parent
25+
.command("list")
26+
.description("List previous Claude Code sessions")
27+
.option("--project <path>", "Project directory filter")
28+
.option("--since <time>", 'Relative time filter (e.g. "1 day ago")')
29+
.option("--after <timestamp>", "Show sessions after this timestamp")
30+
.option("--before <timestamp>", "Show sessions before this timestamp")
31+
.option("-n, --limit <count>", "Maximum number of results", "20")
32+
.option("-f, --format <format>", "Output format: text|json", "text")
33+
.option("--no-color", "Disable colored output")
34+
.action(async (options: ListCommandOptions) => {
35+
try {
36+
if (!options.color) {
37+
chalk.level = 0;
38+
}
39+
40+
const after = options.since
41+
? parseRelativeTime(options.since)
42+
: options.after
43+
? parseTime(options.after)
44+
: undefined;
45+
46+
const before = options.before ? parseTime(options.before) : undefined;
47+
48+
const summaries = await loadHistory({
49+
project: options.project,
50+
after: after ?? undefined,
51+
before: before ?? undefined,
52+
limit: parseInt(options.limit, 10),
53+
});
54+
55+
// Discover session files for enrichment
56+
const sessionFiles = await discoverSessionFiles();
57+
const filesBySessionId = new Map<string, string>();
58+
for (const filePath of sessionFiles) {
59+
// Extract session ID from filename (basename without extension)
60+
const parts = filePath.split("/");
61+
const filename = parts[parts.length - 1];
62+
const id = filename.replace(/\.jsonl$/, "");
63+
filesBySessionId.set(id, filePath);
64+
}
65+
66+
// Enrich summaries with session metadata
67+
const entries: SessionListEntry[] = [];
68+
for (const summary of summaries) {
69+
const filePath = filesBySessionId.get(summary.sessionId);
70+
let meta;
71+
if (filePath) {
72+
try {
73+
meta = await extractSessionMeta(filePath);
74+
} catch {
75+
// Skip enrichment on error
76+
}
77+
}
78+
entries.push({ summary, meta });
79+
}
80+
81+
if (options.format === "json") {
82+
console.log(formatSessionListJson(entries));
83+
} else {
84+
console.log(
85+
formatSessionListText(entries, {
86+
noColor: !options.color,
87+
}),
88+
);
89+
}
90+
} catch (err) {
91+
const message = err instanceof Error ? err.message : String(err);
92+
console.error(`Error: ${message}`);
93+
process.exit(1);
94+
}
95+
});
96+
}

cli/src/commands/session/search.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import chalk from "chalk";
2+
import type { Command } from "commander";
3+
import { formatJson } from "../../output/json.js";
4+
import { formatStats } from "../../output/stats.js";
5+
import { formatText } from "../../output/text.js";
6+
import { search } from "../../search/engine.js";
7+
import { parseRelativeTime, parseTime } from "../../utils/time.js";
8+
9+
interface SearchCommandOptions {
10+
role?: string;
11+
limit: string;
12+
project?: string;
13+
since?: string;
14+
after?: string;
15+
before?: string;
16+
session?: string;
17+
format: string;
18+
stats?: boolean;
19+
color?: boolean;
20+
fullText?: boolean;
21+
pattern?: string;
22+
}
23+
24+
export function registerSearchCommand(parent: Command): void {
25+
parent
26+
.command("search")
27+
.description("Search Claude Code session history")
28+
.argument("[query]", "Search query (supports AND, OR, NOT, quotes)")
29+
.option("-r, --role <role>", "Filter by role (user/assistant/system)")
30+
.option("-n, --limit <count>", "Maximum number of results", "200")
31+
.option("--project <path>", "Project directory filter")
32+
.option("--since <time>", 'Relative time filter (e.g. "1 day ago")')
33+
.option("--after <timestamp>", "Show messages after this timestamp")
34+
.option("--before <timestamp>", "Show messages before this timestamp")
35+
.option("-s, --session <id>", "Filter by session ID")
36+
.option("-f, --format <format>", "Output format: text|json", "text")
37+
.option("--stats", "Show statistics only")
38+
.option("--no-color", "Disable colored output")
39+
.option("--full-text", "Disable content truncation")
40+
.option("-p, --pattern <glob>", "Custom file glob pattern")
41+
.action(
42+
async (query: string | undefined, options: SearchCommandOptions) => {
43+
try {
44+
if (!options.color) {
45+
chalk.level = 0;
46+
}
47+
48+
const after = options.since
49+
? parseRelativeTime(options.since)
50+
: options.after
51+
? parseTime(options.after)
52+
: undefined;
53+
54+
const before = options.before ? parseTime(options.before) : undefined;
55+
56+
const result = await search({
57+
query: query ?? "",
58+
role: options.role,
59+
limit: parseInt(options.limit, 10),
60+
project: options.project,
61+
after: after?.toISOString(),
62+
before: before?.toISOString(),
63+
sessionId: options.session,
64+
pattern: options.pattern,
65+
});
66+
67+
if (options.stats) {
68+
console.log(formatStats(result, { noColor: !options.color }));
69+
} else if (options.format === "json") {
70+
console.log(formatJson(result));
71+
} else {
72+
console.log(
73+
formatText(result, {
74+
fullText: options.fullText,
75+
noColor: !options.color,
76+
}),
77+
);
78+
}
79+
} catch (err) {
80+
const message = err instanceof Error ? err.message : String(err);
81+
console.error(`Error: ${message}`);
82+
process.exit(1);
83+
}
84+
},
85+
);
86+
}

0 commit comments

Comments
 (0)