@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,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const tempRoot = mkdtempSync(path.join(tmpdir(), "agentrecord-smoke-"));
11
+ const packDir = path.join(tempRoot, "pack");
12
+ const installDir = path.join(tempRoot, "install");
13
+ const expectedVersion = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf8")).version;
14
+
15
+ function run(command, args, options = {}) {
16
+ const result = spawnSync(command, args, {
17
+ cwd: options.cwd || repoRoot,
18
+ encoding: "utf8",
19
+ shell: false,
20
+ env: {
21
+ ...process.env,
22
+ npm_config_update_notifier: "false",
23
+ npm_config_fund: "false",
24
+ npm_config_audit: "false"
25
+ }
26
+ });
27
+
28
+ if (result.status !== 0) {
29
+ const stderr = result.stderr ? `\n${result.stderr.trim()}` : "";
30
+ const stdout = result.stdout ? `\n${result.stdout.trim()}` : "";
31
+ throw new Error(`${command} ${args.join(" ")} failed with exit ${result.status}${stdout}${stderr}`);
32
+ }
33
+
34
+ return result.stdout;
35
+ }
36
+
37
+ function assertEqual(actual, expected, label) {
38
+ if (actual.trim() !== expected) {
39
+ throw new Error(`${label} expected ${JSON.stringify(expected)} but received ${JSON.stringify(actual.trim())}`);
40
+ }
41
+ }
42
+
43
+ try {
44
+ mkdirSync(packDir, { recursive: true });
45
+ mkdirSync(installDir, { recursive: true });
46
+ const packOutput = run("npm", ["pack", "--json", "--pack-destination", packDir]);
47
+ const packInfo = JSON.parse(packOutput)[0];
48
+ const tarball = path.join(packDir, packInfo.filename);
49
+
50
+ run("npm", ["install", "--ignore-scripts", tarball], { cwd: installDir });
51
+
52
+ const bin = path.join(installDir, "node_modules", ".bin", process.platform === "win32" ? "agentrecord.cmd" : "agentrecord");
53
+
54
+ assertEqual(run(bin, ["--version"], { cwd: installDir }), expectedVersion, "agentrecord --version");
55
+ assertEqual(run(bin, ["version"], { cwd: installDir }), expectedVersion, "agentrecord version");
56
+
57
+ run(bin, ["init", "--dry-run", "--owner", "smoke-owner"], { cwd: installDir });
58
+ run(bin, ["doctor"], { cwd: installDir });
59
+
60
+ console.log(JSON.stringify({
61
+ ok: true,
62
+ temp_dir: tempRoot,
63
+ tarball: packInfo.filename,
64
+ version: expectedVersion,
65
+ commands: [
66
+ "agentrecord --version",
67
+ "agentrecord version",
68
+ "agentrecord init --dry-run --owner smoke-owner",
69
+ "agentrecord doctor"
70
+ ]
71
+ }, null, 2));
72
+ } finally {
73
+ if (process.env.AGENTRECORD_KEEP_SMOKE_TEMP !== "1") {
74
+ rmSync(tempRoot, { recursive: true, force: true });
75
+ }
76
+ }
@@ -0,0 +1,29 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { renderAgentContextJson, renderAgentContextMarkdown, renderHtml, renderMarkdown, renderRedactionReport, renderRunReport } from "./renderers.mjs";
5
+
6
+ export function writeArtifacts({ config, profile, evidenceCards, localeBundle, privateState, agentContextEnabled }) {
7
+ const outDir = config.resolved.profileDir;
8
+ fs.writeFileSync(path.join(outDir, "profile.json"), `${JSON.stringify(profile, null, 2)}\n`);
9
+ fs.writeFileSync(path.join(outDir, "evidence.jsonl"), `${evidenceCards.map((card) => JSON.stringify(card)).join("\n")}\n`);
10
+ fs.writeFileSync(path.join(outDir, "profile.md"), renderMarkdown(profile, localeBundle));
11
+ fs.writeFileSync(path.join(outDir, "index.html"), renderHtml(profile, localeBundle));
12
+ fs.writeFileSync(path.join(outDir, "redaction-report.md"), renderRedactionReport(profile, localeBundle));
13
+ fs.writeFileSync(path.join(outDir, "run-report.md"), renderRunReport(profile, localeBundle));
14
+ if (agentContextEnabled) {
15
+ fs.writeFileSync(path.join(outDir, "agent-context.md"), renderAgentContextMarkdown(profile, localeBundle));
16
+ fs.writeFileSync(path.join(outDir, "agent-context.json"), `${JSON.stringify(renderAgentContextJson(profile), null, 2)}\n`);
17
+ } else {
18
+ removeGeneratedArtifactIfExists(outDir, "agent-context.md");
19
+ removeGeneratedArtifactIfExists(outDir, "agent-context.json");
20
+ }
21
+ fs.writeFileSync(path.join(config.resolved.privateStateDir, "state.json"), `${JSON.stringify(privateState, null, 2)}\n`);
22
+ removeGeneratedArtifactIfExists(outDir, "hiring.html");
23
+ removeGeneratedArtifactIfExists(outDir, "job-agent-profile.md");
24
+ }
25
+
26
+ function removeGeneratedArtifactIfExists(outDir, file) {
27
+ const target = path.join(outDir, file);
28
+ if (fs.existsSync(target)) fs.rmSync(target);
29
+ }
@@ -0,0 +1,33 @@
1
+
2
+ const supportedLocales = ["en-US", "zh-CN"];
3
+
4
+ const dimensions = [
5
+ { id: "goal_framing", canonical: "Goal Framing" },
6
+ { id: "context_packaging", canonical: "Context Packaging" },
7
+ { id: "agent_delegation", canonical: "Agent Delegation" },
8
+ { id: "review_judgment", canonical: "Review Judgment" },
9
+ { id: "verification_discipline", canonical: "Verification Discipline" },
10
+ { id: "failure_recovery", canonical: "Failure Recovery" },
11
+ { id: "scope_control", canonical: "Scope Control" },
12
+ { id: "shipping_hygiene", canonical: "Shipping Hygiene" },
13
+ { id: "product_judgment", canonical: "Product Judgment" },
14
+ { id: "collaboration_handoff", canonical: "Collaboration Handoff" }
15
+ ];
16
+
17
+ const roles = [
18
+ { id: "product_builder", canonical: "Product Builder" },
19
+ { id: "technical_reviewer", canonical: "Technical Reviewer" },
20
+ { id: "agent_operator", canonical: "Agent Operator" },
21
+ { id: "shipping_owner", canonical: "Shipping Owner" },
22
+ { id: "systems_thinker", canonical: "Systems Thinker" },
23
+ { id: "research_synthesizer", canonical: "Research Synthesizer" },
24
+ { id: "collaboration_handoff", canonical: "Collaboration Handoff" }
25
+ ];
26
+
27
+ const dimensionByCanonical = new Map(dimensions.map((item) => [item.canonical.toLowerCase(), item.id]));
28
+ const roleByCanonical = new Map(roles.map((item) => [item.canonical.toLowerCase(), item.id]));
29
+ roleByCanonical.set("delivery owner", "shipping_owner");
30
+ roleByCanonical.set("ai agent operator", "agent_operator");
31
+ roleByCanonical.set("software engineer", "systems_thinker");
32
+
33
+ export { supportedLocales, dimensions, roles, dimensionByCanonical, roleByCanonical };
@@ -0,0 +1,183 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ const DEFAULT_TIMEOUT_MS = 15000;
4
+
5
+ export async function readCodexAccountUsage(options = {}) {
6
+ const enabled = options.enabled !== false;
7
+ if (!enabled) {
8
+ return {
9
+ status: "disabled",
10
+ source: "codex_app_server",
11
+ summary: null,
12
+ daily_usage_buckets: null
13
+ };
14
+ }
15
+
16
+ const executable = options.executable || "codex";
17
+ const timeoutMs = Number.isFinite(Number(options.timeoutMs))
18
+ ? Math.max(1000, Number(options.timeoutMs))
19
+ : DEFAULT_TIMEOUT_MS;
20
+
21
+ try {
22
+ const result = await callCodexAppServer({ executable, timeoutMs });
23
+ const usage = normalizeUsage(result.usage);
24
+ return {
25
+ status: usage.summary ? "measured" : "unavailable",
26
+ source: "codex_app_server",
27
+ source_label: "Codex CLI account usage",
28
+ account: normalizeAccount(result.account),
29
+ summary: usage.summary,
30
+ daily_usage_buckets: usage.daily_usage_buckets,
31
+ status_reason: usage.summary ? null : "account_usage_unavailable"
32
+ };
33
+ } catch (error) {
34
+ return {
35
+ status: "unavailable",
36
+ source: "codex_app_server",
37
+ source_label: "Codex CLI account usage",
38
+ account: null,
39
+ summary: null,
40
+ daily_usage_buckets: null,
41
+ status_reason: sanitizeReason(error)
42
+ };
43
+ }
44
+ }
45
+
46
+ function callCodexAppServer({ executable, timeoutMs }) {
47
+ return new Promise((resolve, reject) => {
48
+ const child = spawn(executable, ["app-server"], {
49
+ stdio: ["pipe", "pipe", "ignore"]
50
+ });
51
+
52
+ let nextId = 1;
53
+ let buffer = "";
54
+ const pending = new Map();
55
+ const results = {};
56
+ let settled = false;
57
+
58
+ const finish = (error, value) => {
59
+ if (settled) return;
60
+ settled = true;
61
+ clearTimeout(timer);
62
+ child.kill("SIGTERM");
63
+ if (error) reject(error);
64
+ else resolve(value);
65
+ };
66
+
67
+ const timer = setTimeout(() => finish(new Error("timeout")), timeoutMs);
68
+
69
+ const send = (method, params, key) => {
70
+ const id = nextId;
71
+ nextId += 1;
72
+ pending.set(id, key);
73
+ const message = params === undefined ? { id, method } : { id, method, params };
74
+ child.stdin.write(`${JSON.stringify(message)}\n`);
75
+ };
76
+
77
+ const notify = (method, params = {}) => {
78
+ child.stdin.write(`${JSON.stringify({ method, params })}\n`);
79
+ };
80
+
81
+ const maybeDone = () => {
82
+ if (results.account !== undefined && results.usage !== undefined) {
83
+ finish(null, results);
84
+ }
85
+ };
86
+
87
+ child.on("error", (error) => finish(error));
88
+ child.on("exit", (code) => {
89
+ if (!settled && code !== 0) finish(new Error(`app_server_exit_${code}`));
90
+ });
91
+
92
+ child.stdout.on("data", (chunk) => {
93
+ buffer += chunk.toString("utf8");
94
+ let newlineIndex;
95
+ while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
96
+ const line = buffer.slice(0, newlineIndex).trim();
97
+ buffer = buffer.slice(newlineIndex + 1);
98
+ if (!line) continue;
99
+ let message;
100
+ try {
101
+ message = JSON.parse(line);
102
+ } catch {
103
+ continue;
104
+ }
105
+
106
+ if (message.id == null) continue;
107
+ const key = pending.get(message.id);
108
+ if (!key) continue;
109
+
110
+ if (message.error) {
111
+ results[key] = { error: message.error };
112
+ } else {
113
+ results[key] = message.result || null;
114
+ }
115
+
116
+ if (key === "initialize") {
117
+ notify("initialized");
118
+ send("account/read", {}, "account");
119
+ send("account/usage/read", undefined, "usage");
120
+ }
121
+
122
+ maybeDone();
123
+ }
124
+ });
125
+
126
+ send("initialize", {
127
+ clientInfo: {
128
+ name: "agentrecord",
129
+ title: "AgentRecord",
130
+ version: "0.0.0"
131
+ },
132
+ capabilities: {
133
+ experimentalApi: true
134
+ }
135
+ }, "initialize");
136
+ });
137
+ }
138
+
139
+ function normalizeUsage(raw) {
140
+ if (!raw || raw.error) return { summary: null, daily_usage_buckets: null };
141
+ const sourceSummary = raw.summary || {};
142
+ const summary = {
143
+ lifetime_tokens: nullableNumber(sourceSummary.lifetimeTokens),
144
+ peak_daily_tokens: nullableNumber(sourceSummary.peakDailyTokens),
145
+ longest_running_turn_sec: nullableNumber(sourceSummary.longestRunningTurnSec),
146
+ current_streak_days: nullableNumber(sourceSummary.currentStreakDays),
147
+ longest_streak_days: nullableNumber(sourceSummary.longestStreakDays)
148
+ };
149
+ const hasSummary = Object.values(summary).some((value) => value !== null);
150
+ const dailyBuckets = Array.isArray(raw.dailyUsageBuckets)
151
+ ? raw.dailyUsageBuckets.map((bucket) => ({
152
+ start_date: String(bucket.startDate || ""),
153
+ tokens: nullableNumber(bucket.tokens) || 0
154
+ })).filter((bucket) => bucket.start_date)
155
+ : null;
156
+
157
+ return {
158
+ summary: hasSummary ? summary : null,
159
+ daily_usage_buckets: dailyBuckets
160
+ };
161
+ }
162
+
163
+ function normalizeAccount(raw) {
164
+ const account = raw?.account;
165
+ if (!account || raw.error) return null;
166
+ return {
167
+ type: account.type || null,
168
+ plan_type: account.planType || null,
169
+ email_included: false
170
+ };
171
+ }
172
+
173
+ function nullableNumber(value) {
174
+ return Number.isFinite(Number(value)) ? Number(value) : null;
175
+ }
176
+
177
+ function sanitizeReason(error) {
178
+ const message = String(error?.message || error || "unknown");
179
+ if (message === "timeout") return "timeout";
180
+ if (message.startsWith("app_server_exit_")) return message;
181
+ if (message.includes("ENOENT")) return "codex_not_found";
182
+ return "codex_app_server_unavailable";
183
+ }
@@ -0,0 +1,143 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { readJsonIfExists } from "../core/config.mjs";
5
+ import { dimensionByCanonical, roleByCanonical } from "./catalog.mjs";
6
+ import { repoRoot } from "./paths.mjs";
7
+ import { snakeId, unique } from "./utils.mjs";
8
+
9
+ export function loadEvidenceRules(files) {
10
+ const rules = new Map();
11
+ for (const file of files) {
12
+ const parsed = readJsonIfExists(file) || readJsonIfExists(path.join(repoRoot, "references", path.basename(file)));
13
+ if (!Array.isArray(parsed?.rules)) continue;
14
+ for (const rule of parsed.rules) {
15
+ if (!rule?.id) continue;
16
+ rules.set(rule.id, {
17
+ ...rule,
18
+ rule_source_path: `references/${path.basename(file)}`,
19
+ rule_scope: parsed.scope || null
20
+ });
21
+ }
22
+ }
23
+ return [...rules.values()];
24
+ }
25
+
26
+ export function extractMemoryBlocks(file) {
27
+ if (!file || !fs.existsSync(file)) return [];
28
+ const lines = fs.readFileSync(file, "utf8").split(/\n/);
29
+ const starts = [];
30
+ lines.forEach((line, index) => {
31
+ if (/^# Task Group: /.test(line)) starts.push(index);
32
+ });
33
+
34
+ return starts.map((startIndex, position) => {
35
+ const endIndex = position + 1 < starts.length ? starts[position + 1] - 1 : lines.length - 1;
36
+ const bodyLines = lines.slice(startIndex, endIndex + 1);
37
+ return {
38
+ title: lines[startIndex].replace(/^# Task Group:\s*/, "").trim(),
39
+ body: bodyLines.join("\n"),
40
+ start_line: startIndex + 1,
41
+ end_line: endIndex + 1,
42
+ source: path.basename(file)
43
+ };
44
+ });
45
+ }
46
+
47
+ function matchesRule(block, rule) {
48
+ const text = `${block.title}\n${block.body}`.toLowerCase();
49
+ const includeAll = rule.match?.include_all || [];
50
+ const includeAny = rule.match?.include_any || [];
51
+ const excludeAny = rule.match?.exclude_any || [];
52
+ const allIncluded = includeAll.every((pattern) => text.includes(String(pattern).toLowerCase()));
53
+ const anyIncluded = includeAny.length === 0 || includeAny.some((pattern) => text.includes(String(pattern).toLowerCase()));
54
+ const excluded = excludeAny.some((pattern) => text.includes(String(pattern).toLowerCase()));
55
+ return allIncluded && anyIncluded && !excluded;
56
+ }
57
+
58
+ export function buildEvidenceCards({ stats, rules, memoryBlocks, locale }) {
59
+ const cards = [];
60
+
61
+ for (const rule of rules.filter((item) => !item.fallback)) {
62
+ const matches = memoryBlocks.filter((block) => matchesRule(block, rule));
63
+ if (!matches.length) continue;
64
+
65
+ cards.push({
66
+ id: rule.id,
67
+ level: rule.evidence_level || ["E3"],
68
+ title: rule.title,
69
+ category: rule.category,
70
+ summary: locale === "zh-CN"
71
+ ? `${rule.signal_template} 已匹配 ${matches.length} 个精选记忆任务组。`
72
+ : `${rule.signal_template} Matched ${matches.length} curated memory task group${matches.length === 1 ? "" : "s"}.`,
73
+ agent_clients: ["codex"],
74
+ dimensions: normalizeDimensionIds(rule.dimensions),
75
+ role_signals: normalizeRoleIds(rule.role_impacts),
76
+ confidence: confidenceFromLevels(rule.evidence_level),
77
+ refs: [
78
+ ...matches.slice(0, 4).map((block) => ({
79
+ type: "memory",
80
+ source: block.source,
81
+ start_line: block.start_line,
82
+ end_line: block.end_line
83
+ })),
84
+ { type: "metadata", source: "codex_session_metadata" }
85
+ ],
86
+ extraction: {
87
+ rule_id: rule.id,
88
+ rule_source_path: rule.rule_source_path,
89
+ rule_scope: rule.rule_scope
90
+ },
91
+ privacy: {
92
+ public_safe: true,
93
+ mode: "summary_only",
94
+ redacted_fields: ["raw prompts", "raw responses", "raw session ids", "terminal bodies", "source bodies"]
95
+ }
96
+ });
97
+ }
98
+
99
+ const fallbackRule = rules.find((rule) => rule.fallback);
100
+ if (fallbackRule && (cards.length === 0 || stats.files > 0)) {
101
+ cards.push({
102
+ id: fallbackRule.id,
103
+ level: fallbackRule.evidence_level || ["E2"],
104
+ title: fallbackRule.title,
105
+ category: fallbackRule.category,
106
+ summary: locale === "zh-CN"
107
+ ? `${fallbackRule.signal_template} 已扫描 ${stats.files} 个 Codex 本地会话,涉及 ${stats.top_projects.length} 个脱敏项目。`
108
+ : `${fallbackRule.signal_template} Scanned ${stats.files} Codex rollout files across ${stats.top_projects.length} redacted project references.`,
109
+ agent_clients: ["codex"],
110
+ dimensions: normalizeDimensionIds(fallbackRule.dimensions),
111
+ role_signals: normalizeRoleIds(fallbackRule.role_impacts),
112
+ confidence: stats.files > 0 ? "medium" : "low",
113
+ refs: [{ type: "metadata", source: "codex_session_metadata" }],
114
+ extraction: {
115
+ rule_id: fallbackRule.id,
116
+ rule_source_path: fallbackRule.rule_source_path,
117
+ rule_scope: fallbackRule.rule_scope
118
+ },
119
+ privacy: {
120
+ public_safe: true,
121
+ mode: "aggregate_only",
122
+ redacted_fields: ["raw session ids", "session file paths", "private project paths"]
123
+ }
124
+ });
125
+ }
126
+
127
+ return cards;
128
+ }
129
+
130
+ function normalizeDimensionIds(values = []) {
131
+ return unique(values.map((value) => dimensionByCanonical.get(String(value).toLowerCase()) || snakeId(value)));
132
+ }
133
+
134
+ function normalizeRoleIds(values = []) {
135
+ return unique(values.map((value) => roleByCanonical.get(String(value).toLowerCase()) || snakeId(value)));
136
+ }
137
+
138
+ function confidenceFromLevels(levels = []) {
139
+ if (levels.includes("E1") && levels.includes("E2")) return "high";
140
+ if (levels.includes("E1") || levels.includes("E2")) return "medium";
141
+ if (levels.includes("E3")) return "medium";
142
+ return "low";
143
+ }
@@ -0,0 +1,6 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+
6
+ export const repoRoot = path.resolve(__dirname, "..", "..");