@iiwish/agentrecord 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ import path from "node:path";
2
+
3
+ import { normalizeLocale, readJsonIfExists } from "../core/config.mjs";
4
+ import { supportedLocales } from "./catalog.mjs";
5
+ import { repoRoot } from "./paths.mjs";
6
+
7
+ export function resolveReportSettings(config, stats, generatedAt) {
8
+ let locale = config.resolved.report.locale;
9
+ let languageSource = "config";
10
+ let confidence = locale === "auto" ? "low" : "explicit";
11
+
12
+ if (locale === "auto") {
13
+ const agentLocale = inferAgentConversationLocale(stats);
14
+ if (agentLocale) {
15
+ locale = agentLocale.locale;
16
+ languageSource = "agent_conversation_language";
17
+ confidence = agentLocale.confidence;
18
+ } else {
19
+ locale = normalizeLocale(Intl.DateTimeFormat().resolvedOptions().locale, config.resolved.report.fallbackLocale);
20
+ if (locale === "auto") locale = config.resolved.report.fallbackLocale;
21
+ languageSource = "system_locale";
22
+ confidence = "medium";
23
+ }
24
+ }
25
+
26
+ if (!supportedLocales.includes(locale)) {
27
+ locale = config.resolved.report.fallbackLocale;
28
+ languageSource = "fallback_locale";
29
+ confidence = "low";
30
+ }
31
+
32
+ return {
33
+ locale_requested: config.resolved.report.locale,
34
+ locale,
35
+ fallback_locale: config.resolved.report.fallbackLocale,
36
+ label_mode: config.resolved.report.labelMode,
37
+ language_source: languageSource,
38
+ language_confidence: confidence,
39
+ schema_language: "en-US",
40
+ audiences: config.resolved.report.audiences.filter((audience) => ["self", "share"].includes(audience)),
41
+ default_audience: "self",
42
+ supported_locales: supportedLocales,
43
+ agent_language_votes: stats.language_votes,
44
+ agent_language_sample: stats.language_sample,
45
+ generated_at: generatedAt
46
+ };
47
+ }
48
+
49
+ function inferAgentConversationLocale(stats) {
50
+ const zh = stats.language_votes?.["zh-CN"] || 0;
51
+ const en = stats.language_votes?.["en-US"] || 0;
52
+ const total = zh + en;
53
+ if (total < 3) return null;
54
+ const locale = zh >= en ? "zh-CN" : "en-US";
55
+ const share = Math.max(zh, en) / total;
56
+ if (share < 0.6) return null;
57
+ return { locale, confidence: share >= 0.8 ? "high" : "medium" };
58
+ }
59
+
60
+ export function loadLocaleBundle(locale) {
61
+ return readJsonIfExists(path.join(repoRoot, "locales", `${locale}.json`))
62
+ || readJsonIfExists(path.join(repoRoot, "locales", "en-US.json"))
63
+ || { ui: {}, roles: {}, abilities: {}, html_lang: "en" };
64
+ }
@@ -0,0 +1,97 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { readJsonIfExists } from "../core/config.mjs";
5
+ import { ensureDir, stableTimestamp } from "./utils.mjs";
6
+ import { sumTokenUsage, zeroTokenUsage } from "./stats.mjs";
7
+
8
+ export function buildRunContext({ config, stats, generatedAt, reset }) {
9
+ const stateFile = path.join(config.resolved.privateStateDir, "state.json");
10
+ const snapshotsDir = path.join(config.resolved.privateStateDir, "snapshots");
11
+ const previousState = reset ? null : readJsonIfExists(stateFile);
12
+ const previouslyProcessed = new Set(previousState?.processed_session_ids || []);
13
+ const previousTokenTotals = previousState?.session_token_totals || {};
14
+ const hasPreviousTokenTotals = Object.keys(previousTokenTotals).length > 0;
15
+ const newRecords = previousState
16
+ ? stats.session_records.filter((record) => !previouslyProcessed.has(record.session_id))
17
+ : stats.session_records;
18
+ const updatedRecords = previousState && hasPreviousTokenTotals
19
+ ? stats.session_records.filter((record) => {
20
+ if (!previouslyProcessed.has(record.session_id)) return false;
21
+ const currentTokens = record.token_usage?.total_tokens || 0;
22
+ const previousTokens = previousTokenTotals[record.session_id]?.total_tokens || 0;
23
+ return currentTokens > previousTokens;
24
+ })
25
+ : [];
26
+ const tokenDelta = sumTokenUsage(newRecords.filter((record) => record.has_token_usage));
27
+ for (const record of updatedRecords) {
28
+ const current = record.token_usage || {};
29
+ const previous = previousTokenTotals[record.session_id] || {};
30
+ for (const key of Object.keys(tokenDelta)) tokenDelta[key] += Math.max(0, (current[key] || 0) - (previous[key] || 0));
31
+ }
32
+
33
+ let snapshotCreated = false;
34
+ const profileFile = path.join(config.resolved.profileDir, "profile.json");
35
+ if (fs.existsSync(profileFile)) {
36
+ ensureDir(snapshotsDir);
37
+ const previousProfile = readJsonIfExists(profileFile);
38
+ const snapshotName = `profile-${stableTimestamp(previousProfile?.generated_at || generatedAt)}.json`;
39
+ fs.copyFileSync(profileFile, path.join(snapshotsDir, snapshotName));
40
+ snapshotCreated = true;
41
+ }
42
+
43
+ const runCount = reset ? 1 : (previousState?.run_count || 0) + 1;
44
+ const sessionTokenTotals = Object.fromEntries(stats.session_records.map((record) => [
45
+ record.session_id,
46
+ record.token_usage || zeroTokenUsage()
47
+ ]));
48
+
49
+ return {
50
+ public: {
51
+ mode: previousState ? "incremental" : "initial",
52
+ run_count: runCount,
53
+ generated_at: generatedAt,
54
+ previous_generated_at: previousState?.generated_at || null,
55
+ reset,
56
+ new_sessions_this_run: newRecords.length,
57
+ updated_sessions_this_run: updatedRecords.length,
58
+ changed_sessions_this_run: newRecords.length + updatedRecords.length,
59
+ token_delta_this_run: tokenDelta,
60
+ total_sessions_seen: stats.files,
61
+ total_token_sessions_seen: stats.token_sessions,
62
+ private_state_present: true,
63
+ private_snapshot_created: snapshotCreated,
64
+ public_session_ids_included: false
65
+ },
66
+ privateState: {
67
+ schema_version: "agentrecord.state.v0",
68
+ owner: config.resolved.owner,
69
+ generated_at: generatedAt,
70
+ run_count: runCount,
71
+ reset,
72
+ session_roots: config.resolved.codex.sessionRoots,
73
+ trace_window: stats.trace_window,
74
+ processed_sessions_count: stats.session_records.length,
75
+ processed_session_ids: stats.session_records.map((record) => record.session_id).sort(),
76
+ session_token_totals: sessionTokenTotals,
77
+ last_trace_end_timestamp: stats.trace_window.end_timestamp,
78
+ last_profile_hash: null,
79
+ last_run_delta: {
80
+ new_sessions: newRecords.length,
81
+ updated_sessions: updatedRecords.length,
82
+ changed_sessions: newRecords.length + updatedRecords.length,
83
+ token_delta: tokenDelta
84
+ },
85
+ history: [
86
+ ...(previousState?.history || []).slice(-19),
87
+ {
88
+ generated_at: generatedAt,
89
+ mode: previousState ? "incremental" : "initial",
90
+ new_sessions: newRecords.length,
91
+ updated_sessions: updatedRecords.length,
92
+ total_sessions_seen: stats.files
93
+ }
94
+ ]
95
+ }
96
+ };
97
+ }
@@ -0,0 +1,176 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { readJsonlLines } from "./utils.mjs";
5
+
6
+ function collectRolloutFiles(rootDirs) {
7
+ const files = [];
8
+ const roots = Array.isArray(rootDirs) ? rootDirs : [rootDirs].filter(Boolean);
9
+
10
+ for (const root of roots) {
11
+ if (!root || !fs.existsSync(root)) continue;
12
+ const stack = [root];
13
+ while (stack.length) {
14
+ const current = stack.pop();
15
+ let entries = [];
16
+ try {
17
+ entries = fs.readdirSync(current, { withFileTypes: true });
18
+ } catch {
19
+ continue;
20
+ }
21
+ for (const entry of entries) {
22
+ const target = path.join(current, entry.name);
23
+ if (entry.isDirectory()) {
24
+ stack.push(target);
25
+ } else if (entry.isFile() && /^rollout-.*\.jsonl$/.test(entry.name)) {
26
+ files.push(target);
27
+ }
28
+ }
29
+ }
30
+ }
31
+
32
+ return [...new Set(files)].sort();
33
+ }
34
+
35
+ function extractUserText(obj) {
36
+ if (obj.type !== "response_item" || obj.payload?.type !== "message" || obj.payload?.role !== "user") return "";
37
+ if (!Array.isArray(obj.payload.content)) return "";
38
+ return obj.payload.content
39
+ .filter((item) => ["input_text", "output_text", "text"].includes(item?.type))
40
+ .map((item) => item.text || "")
41
+ .join("\n");
42
+ }
43
+
44
+ function countLanguageSignal(text) {
45
+ const sample = String(text || "").slice(0, 12000);
46
+ const cjk = sample.match(/[\u3400-\u9fff]/g)?.length || 0;
47
+ const latinWords = sample.match(/[A-Za-z]{2,}/g)?.length || 0;
48
+ if (cjk >= 12 && cjk >= latinWords * 0.25) return "zh-CN";
49
+ if (latinWords >= 30 && cjk < 8) return "en-US";
50
+ return null;
51
+ }
52
+
53
+ export function sumTokenUsage(records) {
54
+ return records.reduce((total, record) => {
55
+ const usage = record.token_usage || {};
56
+ for (const key of Object.keys(total)) total[key] += usage[key] || 0;
57
+ return total;
58
+ }, zeroTokenUsage());
59
+ }
60
+
61
+ export function zeroTokenUsage() {
62
+ return {
63
+ total_tokens: 0,
64
+ input_tokens: 0,
65
+ cached_input_tokens: 0,
66
+ output_tokens: 0,
67
+ reasoning_output_tokens: 0
68
+ };
69
+ }
70
+
71
+ export function collectCodexStats(rootDirs, { publicProjectPaths }) {
72
+ const files = collectRolloutFiles(rootDirs);
73
+ const byCwd = new Map();
74
+ const bySource = new Map();
75
+ const projectTokens = new Map();
76
+ const sessionRecords = [];
77
+ const languageVotes = { "zh-CN": 0, "en-US": 0 };
78
+ const languageSample = { user_messages_seen: 0, sampled_characters: 0 };
79
+ const totals = zeroTokenUsage();
80
+ let tokenSessions = 0;
81
+ let minTs = null;
82
+ let maxTs = null;
83
+
84
+ for (const file of files) {
85
+ let meta = null;
86
+ let lastToken = null;
87
+
88
+ for (const line of readJsonlLines(file)) {
89
+ let obj;
90
+ try {
91
+ obj = JSON.parse(line);
92
+ } catch {
93
+ continue;
94
+ }
95
+
96
+ if (obj.type === "session_meta" && obj.payload) meta = obj.payload;
97
+ if (obj.type === "event_msg" && obj.payload?.type === "token_count") {
98
+ lastToken = obj.payload.info?.total_token_usage || null;
99
+ }
100
+
101
+ if (languageSample.user_messages_seen < 500) {
102
+ const userText = extractUserText(obj);
103
+ if (userText) {
104
+ languageSample.user_messages_seen += 1;
105
+ languageSample.sampled_characters += Math.min(userText.length, 12000);
106
+ const detected = countLanguageSignal(userText);
107
+ if (detected) languageVotes[detected] += 1;
108
+ }
109
+ }
110
+ }
111
+
112
+ const cwd = meta?.cwd || "(unknown)";
113
+ const source = meta?.source || meta?.originator || "(unknown)";
114
+ byCwd.set(cwd, (byCwd.get(cwd) || 0) + 1);
115
+ bySource.set(source, (bySource.get(source) || 0) + 1);
116
+
117
+ if (meta?.timestamp) {
118
+ if (!minTs || meta.timestamp < minTs) minTs = meta.timestamp;
119
+ if (!maxTs || meta.timestamp > maxTs) maxTs = meta.timestamp;
120
+ }
121
+
122
+ if (lastToken?.total_tokens) {
123
+ tokenSessions += 1;
124
+ for (const key of Object.keys(totals)) totals[key] += lastToken[key] || 0;
125
+ projectTokens.set(cwd, (projectTokens.get(cwd) || 0) + (lastToken.total_tokens || 0));
126
+ }
127
+
128
+ let fileMtimeMs = 0;
129
+ try {
130
+ fileMtimeMs = Math.round(fs.statSync(file).mtimeMs);
131
+ } catch {
132
+ fileMtimeMs = 0;
133
+ }
134
+
135
+ sessionRecords.push({
136
+ session_id: meta?.id || path.basename(file).replace(/^rollout-|\.jsonl$/g, ""),
137
+ timestamp: meta?.timestamp || null,
138
+ project_path: cwd,
139
+ source,
140
+ has_token_usage: Boolean(lastToken?.total_tokens),
141
+ token_usage: lastToken || null,
142
+ file_mtime_ms: fileMtimeMs
143
+ });
144
+ }
145
+
146
+ const projectRows = [...byCwd.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12);
147
+ const topProjects = projectRows.map(([projectPath, sessions], index) => ({
148
+ project_ref: publicProjectPaths ? safeProjectName(projectPath) : `project_ref_${String(index + 1).padStart(3, "0")}`,
149
+ sessions,
150
+ total_tokens: projectTokens.get(projectPath) || 0,
151
+ public_project_path_included: Boolean(publicProjectPaths)
152
+ }));
153
+
154
+ return {
155
+ session_roots_count: Array.isArray(rootDirs) ? rootDirs.length : 0,
156
+ files: files.length,
157
+ token_sessions: tokenSessions,
158
+ trace_window: {
159
+ start: minTs ? minTs.slice(0, 10) : "unknown",
160
+ end: maxTs ? maxTs.slice(0, 10) : "unknown",
161
+ start_timestamp: minTs,
162
+ end_timestamp: maxTs
163
+ },
164
+ total_token_usage: totals,
165
+ language_votes: languageVotes,
166
+ language_sample: languageSample,
167
+ by_source: Object.fromEntries([...bySource.entries()].sort((a, b) => b[1] - a[1])),
168
+ top_projects: topProjects,
169
+ session_records: sessionRecords
170
+ };
171
+ }
172
+
173
+ function safeProjectName(projectPath) {
174
+ if (!projectPath || projectPath === "(unknown)") return "unknown";
175
+ return path.basename(projectPath);
176
+ }
@@ -0,0 +1,50 @@
1
+ import fs from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+
4
+ export function ensureDir(dir) {
5
+ fs.mkdirSync(dir, { recursive: true });
6
+ }
7
+
8
+ export function readJsonlLines(file) {
9
+ try {
10
+ return fs.readFileSync(file, "utf8").split(/\n/).filter(Boolean);
11
+ } catch {
12
+ return [];
13
+ }
14
+ }
15
+
16
+ export function hashObject(value) {
17
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
18
+ }
19
+
20
+ export function stableTimestamp(value) {
21
+ return String(value || new Date().toISOString()).replace(/[:.]/g, "-");
22
+ }
23
+
24
+ export function snakeId(value) {
25
+ return String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
26
+ }
27
+
28
+ export function unique(values) {
29
+ return [...new Set(values.filter(Boolean))];
30
+ }
31
+
32
+ export function formatNumber(value, locale = "en-US") {
33
+ return new Intl.NumberFormat(locale).format(value || 0);
34
+ }
35
+
36
+ export function formatCompactNumber(value, locale = "en-US") {
37
+ return new Intl.NumberFormat(locale, {
38
+ maximumFractionDigits: 1,
39
+ notation: "compact"
40
+ }).format(value || 0);
41
+ }
42
+
43
+ export function esc(value) {
44
+ return String(value ?? "")
45
+ .replaceAll("&", "&amp;")
46
+ .replaceAll("<", "&lt;")
47
+ .replaceAll(">", "&gt;")
48
+ .replaceAll("\"", "&quot;")
49
+ .replaceAll("'", "&#39;");
50
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+
5
+ import { runBuild } from "./commands/build.mjs";
6
+ import { runDoctor } from "./commands/doctor.mjs";
7
+ import { runInit } from "./commands/init.mjs";
8
+ import { runOpen } from "./commands/open.mjs";
9
+ import { runScan } from "./commands/scan.mjs";
10
+ import { runValidate } from "./commands/validate.mjs";
11
+ import { parseArgs } from "./core/args.mjs";
12
+
13
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
14
+ const VERSION = packageJson.version;
15
+
16
+ const help = `AgentRecord ${VERSION}
17
+
18
+ Local-first, auditable AI work profiles from agent traces.
19
+
20
+ Usage:
21
+ agentrecord --help
22
+ agentrecord --version
23
+ agentrecord doctor
24
+ agentrecord init [--dry-run] [--owner <name>] [--profiles-dir <dir>] [--output <dir>]
25
+ agentrecord scan [--config <file>] [--sessions-dir <dir>]
26
+ agentrecord build [--config <file>] [--agent-context] [--no-account-usage]
27
+ agentrecord validate [--config <file>]
28
+ agentrecord open [--config <file>] [--owner <owner>]
29
+
30
+ Commands:
31
+ agentrecord doctor Check local runtime and source availability
32
+ agentrecord init Create agentrecord.config.json
33
+ agentrecord scan Discover local AI-agent trace sources
34
+ agentrecord build Generate profile.json, evidence.jsonl, and HTML report
35
+ agentrecord validate Check schema, privacy boundary, and report integrity
36
+ agentrecord open Open profiles/<owner>/index.html
37
+
38
+ Build options:
39
+ --no-account-usage Skip Codex CLI account usage lookup
40
+ --account-usage-timeout-ms <ms> Timeout for Codex CLI account usage lookup
41
+ `;
42
+
43
+ const { options, positional } = parseArgs(process.argv.slice(2));
44
+ const command = positional[0];
45
+
46
+ if (options.version || command === "version") {
47
+ console.log(VERSION);
48
+ process.exit(0);
49
+ }
50
+
51
+ if (options.help || !command || command === "help") {
52
+ console.log(help);
53
+ process.exit(0);
54
+ }
55
+
56
+ const commands = {
57
+ doctor: runDoctor,
58
+ init: runInit,
59
+ scan: runScan,
60
+ build: runBuild,
61
+ validate: runValidate,
62
+ open: runOpen
63
+ };
64
+
65
+ const handler = commands[command];
66
+
67
+ if (handler) {
68
+ await handler({
69
+ version: VERSION,
70
+ options,
71
+ args: positional.slice(1)
72
+ });
73
+ process.exit(process.exitCode || 0);
74
+ }
75
+
76
+ console.error(`Unknown command: ${command}`);
77
+ console.error("Run `agentrecord --help` for usage.");
78
+ process.exit(1);
@@ -0,0 +1,75 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { loadConfig } from "../core/config.mjs";
4
+ import { writeArtifacts } from "../build/artifacts.mjs";
5
+ import { readCodexAccountUsage } from "../build/codex-account.mjs";
6
+ import { buildEvidenceCards, extractMemoryBlocks, loadEvidenceRules } from "../build/evidence.mjs";
7
+ import { buildProfile } from "../build/profile.mjs";
8
+ import { loadLocaleBundle, resolveReportSettings } from "../build/report.mjs";
9
+ import { buildRunContext } from "../build/run-context.mjs";
10
+ import { collectCodexStats } from "../build/stats.mjs";
11
+ import { ensureDir, hashObject } from "../build/utils.mjs";
12
+
13
+ export async function runBuild({ options }) {
14
+ const config = loadConfig(options);
15
+ const generatedAt = new Date().toISOString();
16
+ const agentContextEnabled = options.agentContext === true;
17
+ ensureDir(config.resolved.profileDir);
18
+ ensureDir(config.resolved.privateStateDir);
19
+
20
+ const stats = collectCodexStats(config.resolved.codex.sessionRoots, {
21
+ publicProjectPaths: config.resolved.privacy.publicProjectPaths
22
+ });
23
+ const codexAccountUsage = await readCodexAccountUsage(config.resolved.codex.accountUsage);
24
+ const report = resolveReportSettings(config, stats, generatedAt);
25
+ const localeBundle = loadLocaleBundle(report.locale);
26
+
27
+ let rulePaths = config.resolved.evidenceRulesPaths;
28
+ if (report.locale === "zh-CN") {
29
+ rulePaths = rulePaths.map((filePath) => {
30
+ const parent = path.dirname(filePath);
31
+ const ext = path.extname(filePath);
32
+ const base = path.basename(filePath, ext);
33
+ const zhPath = path.join(parent, `${base}.zh-CN${ext}`);
34
+ return fs.existsSync(zhPath) ? zhPath : filePath;
35
+ });
36
+ }
37
+ const rules = loadEvidenceRules(rulePaths);
38
+
39
+ const memoryBlocks = config.resolved.memory.enabled ? extractMemoryBlocks(config.resolved.memory.registryPath) : [];
40
+ const evidenceCards = buildEvidenceCards({ stats, rules, memoryBlocks, locale: report.locale });
41
+ const runContext = buildRunContext({ config, stats, generatedAt, reset: Boolean(options.reset) });
42
+ const profile = buildProfile({
43
+ config,
44
+ stats,
45
+ codexAccountUsage,
46
+ evidenceCards,
47
+ report,
48
+ localeBundle,
49
+ runMetadata: runContext.public,
50
+ agentContextEnabled
51
+ });
52
+
53
+ runContext.privateState.last_profile_hash = hashObject(profile);
54
+ writeArtifacts({ config, profile, evidenceCards, localeBundle, privateState: runContext.privateState, agentContextEnabled });
55
+
56
+ console.log(JSON.stringify({
57
+ ok: true,
58
+ out_dir: config.resolved.profileDir,
59
+ owner: config.resolved.owner,
60
+ schema_version: profile.schema_version,
61
+ sessions_scanned: stats.files,
62
+ codex_account_usage: codexAccountUsage.status,
63
+ evidence_cards: evidenceCards.length,
64
+ report_locale: report.locale,
65
+ artifacts: [
66
+ "profile.json",
67
+ "evidence.jsonl",
68
+ "index.html",
69
+ "profile.md",
70
+ "redaction-report.md",
71
+ "run-report.md",
72
+ ...(agentContextEnabled ? ["agent-context.md", "agent-context.json"] : [])
73
+ ]
74
+ }, null, 2));
75
+ }
@@ -0,0 +1,20 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ export async function runDoctor({ version }) {
6
+ const defaultCodexSessionsDir = path.join(os.homedir(), ".codex", "sessions");
7
+
8
+ console.log(JSON.stringify({
9
+ ok: true,
10
+ package: "agentrecord",
11
+ version,
12
+ node: process.version,
13
+ cwd: process.cwd(),
14
+ config_exists: fs.existsSync(path.resolve(process.cwd(), "agentrecord.config.json")),
15
+ codex_sessions_default: {
16
+ path: defaultCodexSessionsDir,
17
+ exists: fs.existsSync(defaultCodexSessionsDir)
18
+ }
19
+ }, null, 2));
20
+ }
@@ -0,0 +1,62 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { CONFIG_FILE, createDefaultConfig, defaultOwnerDisplayName, safePathSegment, writeJson } from "../core/config.mjs";
5
+
6
+ export async function runInit({ options }) {
7
+ const ownerDisplayName = options.owner || defaultOwnerDisplayName();
8
+ const owner = safePathSegment(ownerDisplayName);
9
+ const profilesDir = options.profilesDir || "profiles";
10
+ const profileDir = options.output || path.join(profilesDir, owner);
11
+ const config = createDefaultConfig({
12
+ owner: ownerDisplayName,
13
+ profilesDir,
14
+ locale: options.locale || "auto"
15
+ });
16
+
17
+ config.output.profile_dir = profileDir;
18
+
19
+ if (options.codexSessionsDir || options.sessionsDir) {
20
+ const sessionsDir = options.codexSessionsDir || options.sessionsDir;
21
+ config.codex.sessions_dir = sessionsDir;
22
+ config.codex.session_roots = [sessionsDir];
23
+ }
24
+
25
+ if (typeof options.accountUsage === "boolean") config.codex.account_usage.enabled = options.accountUsage;
26
+ if (options.accountUsageTimeoutMs) config.codex.account_usage.timeout_ms = Number(options.accountUsageTimeoutMs);
27
+
28
+ if (options.publicProjectPaths === true) config.privacy.public_project_paths = true;
29
+ if (options.publicProjectPaths === false) config.privacy.public_project_paths = false;
30
+ if (options.privacy) config.privacy.mode = options.privacy;
31
+
32
+ const target = path.resolve(process.cwd(), options.config || CONFIG_FILE);
33
+
34
+ if (options.dryRun) {
35
+ console.log(JSON.stringify({
36
+ ok: true,
37
+ dry_run: true,
38
+ would_write: target,
39
+ config
40
+ }, null, 2));
41
+ return;
42
+ }
43
+
44
+ if (fs.existsSync(target) && !options.force) {
45
+ console.log(JSON.stringify({
46
+ ok: true,
47
+ created: false,
48
+ exists: true,
49
+ path: target,
50
+ message: "Config already exists. Use --force to overwrite."
51
+ }, null, 2));
52
+ return;
53
+ }
54
+
55
+ writeJson(target, config);
56
+ console.log(JSON.stringify({
57
+ ok: true,
58
+ created: true,
59
+ path: target,
60
+ config
61
+ }, null, 2));
62
+ }