@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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/examples/basic/README.md +23 -0
- package/examples/basic/agentrecord.config.example.json +36 -0
- package/locales/en-US.json +45 -0
- package/locales/zh-CN.json +45 -0
- package/package.json +36 -0
- package/references/evidence-rules.json +112 -0
- package/references/evidence-rules.zh-CN.json +112 -0
- package/schemas/evidence.schema.json +30 -0
- package/schemas/profile.schema.json +34 -0
- package/scripts/smoke-install.mjs +76 -0
- package/src/build/artifacts.mjs +29 -0
- package/src/build/catalog.mjs +33 -0
- package/src/build/codex-account.mjs +183 -0
- package/src/build/evidence.mjs +143 -0
- package/src/build/paths.mjs +6 -0
- package/src/build/profile.mjs +608 -0
- package/src/build/renderers.mjs +1267 -0
- package/src/build/report.mjs +64 -0
- package/src/build/run-context.mjs +97 -0
- package/src/build/stats.mjs +176 -0
- package/src/build/utils.mjs +50 -0
- package/src/cli.mjs +78 -0
- package/src/commands/build.mjs +75 -0
- package/src/commands/doctor.mjs +20 -0
- package/src/commands/init.mjs +62 -0
- package/src/commands/open.mjs +81 -0
- package/src/commands/scan.mjs +58 -0
- package/src/commands/validate.mjs +365 -0
- package/src/core/args.mjs +60 -0
- package/src/core/config.mjs +228 -0
|
@@ -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("&", "&")
|
|
46
|
+
.replaceAll("<", "<")
|
|
47
|
+
.replaceAll(">", ">")
|
|
48
|
+
.replaceAll("\"", """)
|
|
49
|
+
.replaceAll("'", "'");
|
|
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
|
+
}
|