@osfactory/har 0.13.1 → 0.14.0

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.
Files changed (2) hide show
  1. package/dist/index.js +204 -34
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -23129,6 +23129,22 @@ var AgentSessionUsageSchema = external_exports.object({
23129
23129
  var SyncUsageInputSchema = external_exports.object({
23130
23130
  usage: external_exports.array(AgentSessionUsageSchema)
23131
23131
  });
23132
+ var AgentSessionEventSchema = external_exports.object({
23133
+ sessionKey: external_exports.string().min(1),
23134
+ agentId: external_exports.number().int().min(HAR_AGENT_SLOT_MIN),
23135
+ agentTool: AgentToolSchema,
23136
+ eventName: external_exports.string().min(1),
23137
+ sequence: external_exports.number().int().nonnegative().default(0),
23138
+ timestamp: external_exports.string(),
23139
+ attributes: external_exports.record(external_exports.unknown()).optional(),
23140
+ promptText: external_exports.string().nullable().optional(),
23141
+ responseText: external_exports.string().nullable().optional(),
23142
+ rawTruncated: external_exports.string().nullable().optional(),
23143
+ source: external_exports.enum(["otel", "harvest"]).default("otel")
23144
+ });
23145
+ var SyncSessionEventsInputSchema = external_exports.object({
23146
+ events: external_exports.array(AgentSessionEventSchema)
23147
+ });
23132
23148
 
23133
23149
  // src/harness/manifest.ts
23134
23150
  var GENERATOR_VERSION = "0.4.0";
@@ -29915,6 +29931,18 @@ function createRemoteExecutor(apiUrl, apiKey) {
29915
29931
  var fs33 = __toESM(require("fs"));
29916
29932
  var os9 = __toESM(require("os"));
29917
29933
  var path33 = __toESM(require("path"));
29934
+ var DEFAULT_SIGNALS_ON = {
29935
+ metrics: true,
29936
+ logs: true,
29937
+ prompts: false,
29938
+ traces: false
29939
+ };
29940
+ var DEFAULT_SIGNALS_OFF = {
29941
+ metrics: false,
29942
+ logs: false,
29943
+ prompts: false,
29944
+ traces: false
29945
+ };
29918
29946
  function getPreferencePath() {
29919
29947
  if (process.env.HAR_TELEMETRY_CONFIG_PATH) {
29920
29948
  return path33.resolve(process.env.HAR_TELEMETRY_CONFIG_PATH);
@@ -29928,24 +29956,41 @@ function parseEnvOverride(raw) {
29928
29956
  if (["1", "true", "on", "yes", "enabled"].includes(normalized)) return true;
29929
29957
  return void 0;
29930
29958
  }
29959
+ function normalizeSignals(enabled, raw) {
29960
+ if (!enabled) return { ...DEFAULT_SIGNALS_OFF };
29961
+ return {
29962
+ metrics: raw?.metrics !== false,
29963
+ logs: raw?.logs !== false,
29964
+ prompts: raw?.prompts === true,
29965
+ traces: raw?.traces === true
29966
+ };
29967
+ }
29931
29968
  function readTelemetryPreference() {
29932
29969
  const preferencePath = getPreferencePath();
29933
29970
  try {
29934
29971
  if (!fs33.existsSync(preferencePath)) {
29935
- return { enabled: true };
29972
+ return { enabled: true, signals: { ...DEFAULT_SIGNALS_ON } };
29936
29973
  }
29937
29974
  const parsed = JSON.parse(fs33.readFileSync(preferencePath, "utf8"));
29975
+ const enabled = parsed.enabled !== false;
29938
29976
  return {
29939
- enabled: parsed.enabled !== false,
29977
+ enabled,
29978
+ signals: normalizeSignals(enabled, parsed.signals),
29940
29979
  updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : void 0
29941
29980
  };
29942
29981
  } catch {
29943
- return { enabled: true };
29982
+ return { enabled: true, signals: { ...DEFAULT_SIGNALS_ON } };
29944
29983
  }
29945
29984
  }
29946
- function writeTelemetryPreference(enabled) {
29985
+ function writeTelemetryPreference(enabled, signals) {
29986
+ const current = readTelemetryPreference();
29987
+ const nextSignals = normalizeSignals(enabled, {
29988
+ ...current.signals,
29989
+ ...signals
29990
+ });
29947
29991
  const preference = {
29948
29992
  enabled,
29993
+ signals: enabled ? nextSignals : { ...DEFAULT_SIGNALS_OFF },
29949
29994
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
29950
29995
  };
29951
29996
  const preferencePath = getPreferencePath();
@@ -29958,12 +30003,18 @@ function isTelemetryEnabled() {
29958
30003
  if (envOverride !== void 0) return envOverride;
29959
30004
  return readTelemetryPreference().enabled;
29960
30005
  }
30006
+ function getTelemetrySignals() {
30007
+ if (!isTelemetryEnabled()) return { ...DEFAULT_SIGNALS_OFF };
30008
+ return readTelemetryPreference().signals;
30009
+ }
29961
30010
  function getTelemetryPreferencePath() {
29962
30011
  return getPreferencePath();
29963
30012
  }
29964
30013
  var TELEMETRY_SIGNALS = [
29965
- "Claude Code: tokens (input/output/cache) and estimated USD cost via OTEL metrics",
29966
- "Codex CLI: token usage via OTEL metrics (no native USD; harvest fills gaps)",
30014
+ "Metrics (default): tokens and estimated USD cost via OTEL metrics",
30015
+ "Logs/events (default): session events (tool calls, api_request) without prompt bodies",
30016
+ "Prompts (opt-in): user/assistant text via OTEL_LOG_USER_PROMPTS \u2014 har telemetry on --prompts",
30017
+ "Traces (opt-in): thin span ingest when CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1",
29967
30018
  "Fallback: har control sync harvests local Claude/Codex session files when OTEL is missing"
29968
30019
  ];
29969
30020
 
@@ -30006,13 +30057,31 @@ function buildTelemetryEnvBlock(attrs, options) {
30006
30057
  ];
30007
30058
  const injectOtel = isTelemetryEnabled() && options?.otelReady !== false;
30008
30059
  if (injectOtel) {
30060
+ const signals = getTelemetrySignals();
30009
30061
  lines.push(
30010
30062
  "# HAR telemetry \u2192 Mission Control OTLP (disable: har telemetry off)",
30011
30063
  "CLAUDE_CODE_ENABLE_TELEMETRY=1",
30012
- "OTEL_METRICS_EXPORTER=otlp",
30013
30064
  "OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
30014
30065
  `OTEL_EXPORTER_OTLP_ENDPOINT=${apiUrl}/api/otel`
30015
30066
  );
30067
+ if (signals.metrics) {
30068
+ lines.push("OTEL_METRICS_EXPORTER=otlp");
30069
+ }
30070
+ if (signals.logs) {
30071
+ lines.push("OTEL_LOGS_EXPORTER=otlp");
30072
+ }
30073
+ if (signals.prompts) {
30074
+ lines.push(
30075
+ "OTEL_LOG_USER_PROMPTS=1",
30076
+ "OTEL_LOG_ASSISTANT_RESPONSES=1"
30077
+ );
30078
+ }
30079
+ if (signals.traces) {
30080
+ lines.push(
30081
+ "OTEL_TRACES_EXPORTER=otlp",
30082
+ "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1"
30083
+ );
30084
+ }
30016
30085
  }
30017
30086
  return lines.join("\n") + "\n";
30018
30087
  }
@@ -30111,19 +30180,12 @@ function extractClaudeUsageFromRecords(records) {
30111
30180
  }
30112
30181
  return { tokensInput, tokensOutput, tokensCacheRead, tokensCacheCreation, costUsd };
30113
30182
  }
30114
- function harvestClaudeUsage(slot) {
30183
+ function findMatchingClaudeTranscripts(slot) {
30115
30184
  const targets = [slot.workDir, slot.worktreePath].filter(Boolean);
30116
- if (targets.length === 0) return null;
30185
+ if (targets.length === 0) return [];
30117
30186
  const root = claudeProjectsRoot();
30118
- if (!fs35.existsSync(root)) return null;
30119
- const sessionKey = buildSessionKey({
30120
- branch: slot.branch,
30121
- agentId: slot.agentId,
30122
- suffix: slot.suffix,
30123
- createdAt: slot.sessionCreatedAt
30124
- });
30125
- let best = null;
30126
- let bestMtime = 0;
30187
+ if (!fs35.existsSync(root)) return [];
30188
+ const matches = [];
30127
30189
  for (const entry of fs35.readdirSync(root, { withFileTypes: true })) {
30128
30190
  if (!entry.isDirectory()) continue;
30129
30191
  const projectDir = path35.join(root, entry.name);
@@ -30143,16 +30205,70 @@ function harvestClaudeUsage(slot) {
30143
30205
  }
30144
30206
  }
30145
30207
  if (!cwdHit) continue;
30146
- const usage2 = extractClaudeUsageFromRecords(records);
30147
- if (usage2.tokensInput + usage2.tokensOutput + usage2.tokensCacheRead === 0 && (usage2.costUsd == null || usage2.costUsd === 0)) {
30148
- continue;
30149
- }
30150
- if (stat.mtimeMs >= bestMtime) {
30151
- bestMtime = stat.mtimeMs;
30152
- best = usage2;
30153
- }
30208
+ matches.push({ filePath, records, mtimeMs: stat.mtimeMs });
30154
30209
  }
30155
30210
  }
30211
+ return matches.sort((a2, b2) => b2.mtimeMs - a2.mtimeMs);
30212
+ }
30213
+ function extractPromptEvents(records, sessionKey, slot) {
30214
+ const events = [];
30215
+ let sequence = 0;
30216
+ for (const record2 of records) {
30217
+ if (!record2 || typeof record2 !== "object") continue;
30218
+ const payload = record2;
30219
+ const typ = String(payload.type ?? "");
30220
+ const message = payload.message;
30221
+ const role = String(message?.role ?? payload.role ?? "");
30222
+ let text = null;
30223
+ const content = message?.content ?? payload.content;
30224
+ if (typeof content === "string") text = content;
30225
+ else if (Array.isArray(content)) {
30226
+ text = content.map((part) => {
30227
+ if (typeof part === "string") return part;
30228
+ if (part && typeof part === "object" && typeof part.text === "string") {
30229
+ return part.text;
30230
+ }
30231
+ return "";
30232
+ }).filter(Boolean).join("\n");
30233
+ }
30234
+ if (!text || text.trim().length === 0) continue;
30235
+ const isUser = typ === "user" || role === "user" || typ === "human" || Boolean(payload.userType);
30236
+ const isAssistant = typ === "assistant" || role === "assistant";
30237
+ if (!isUser && !isAssistant) continue;
30238
+ sequence += 1;
30239
+ const timestamp = typeof payload.timestamp === "string" ? payload.timestamp : typeof payload.ts === "string" ? payload.ts : (/* @__PURE__ */ new Date()).toISOString();
30240
+ events.push({
30241
+ sessionKey,
30242
+ agentId: slot.agentId,
30243
+ agentTool: "claude_code",
30244
+ eventName: isUser ? "claude_code.user_prompt" : "claude_code.assistant_response",
30245
+ sequence,
30246
+ timestamp,
30247
+ promptText: isUser ? text.slice(0, 8e3) : null,
30248
+ responseText: isAssistant ? text.slice(0, 8e3) : null,
30249
+ rawTruncated: text.slice(0, 4e3),
30250
+ source: "harvest"
30251
+ });
30252
+ }
30253
+ return events;
30254
+ }
30255
+ function harvestClaudeUsage(slot) {
30256
+ const matches = findMatchingClaudeTranscripts(slot);
30257
+ const sessionKey = buildSessionKey({
30258
+ branch: slot.branch,
30259
+ agentId: slot.agentId,
30260
+ suffix: slot.suffix,
30261
+ createdAt: slot.sessionCreatedAt
30262
+ });
30263
+ let best = null;
30264
+ for (const match of matches) {
30265
+ const usage2 = extractClaudeUsageFromRecords(match.records);
30266
+ if (usage2.tokensInput + usage2.tokensOutput + usage2.tokensCacheRead === 0 && (usage2.costUsd == null || usage2.costUsd === 0)) {
30267
+ continue;
30268
+ }
30269
+ best = usage2;
30270
+ break;
30271
+ }
30156
30272
  if (!best) return null;
30157
30273
  const now = (/* @__PURE__ */ new Date()).toISOString();
30158
30274
  const tokensTotal = best.tokensInput + best.tokensOutput + best.tokensCacheRead + best.tokensCacheCreation;
@@ -30174,6 +30290,18 @@ function harvestClaudeUsage(slot) {
30174
30290
  lastSeenAt: now
30175
30291
  };
30176
30292
  }
30293
+ function harvestClaudeEvents(slot) {
30294
+ if (!getTelemetrySignals().prompts) return [];
30295
+ const matches = findMatchingClaudeTranscripts(slot);
30296
+ if (matches.length === 0) return [];
30297
+ const sessionKey = buildSessionKey({
30298
+ branch: slot.branch,
30299
+ agentId: slot.agentId,
30300
+ suffix: slot.suffix,
30301
+ createdAt: slot.sessionCreatedAt
30302
+ });
30303
+ return extractPromptEvents(matches[0].records, sessionKey, slot).slice(-40);
30304
+ }
30177
30305
 
30178
30306
  // src/core/usage-harvest/codex.ts
30179
30307
  var fs36 = __toESM(require("fs"));
@@ -30300,6 +30428,9 @@ function harvestUsageForSlot(slot) {
30300
30428
  if (codex) out.push(codex);
30301
30429
  return out;
30302
30430
  }
30431
+ function harvestEventsForSlot(slot) {
30432
+ return harvestClaudeEvents(slot);
30433
+ }
30303
30434
 
30304
30435
  // src/core/control-sync.ts
30305
30436
  async function postJson(url, body, dryRun) {
@@ -30454,6 +30585,21 @@ async function syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun) {
30454
30585
  const usageBody = SyncUsageInputSchema.parse({ usage: usage2 });
30455
30586
  await postJson(`${apiUrl}/api/repos/${repoId}/usage`, usageBody, dryRun);
30456
30587
  }
30588
+ const events = status.slots.flatMap(
30589
+ (slot) => harvestEventsForSlot({
30590
+ agentId: slot.agentId,
30591
+ workDir: slot.workDir,
30592
+ worktreePath: slot.worktreePath,
30593
+ branch: slot.branch,
30594
+ suffix: slot.suffix,
30595
+ sessionCreatedAt: slot.sessionCreatedAt,
30596
+ repoPath
30597
+ })
30598
+ );
30599
+ if (events.length > 0) {
30600
+ const eventsBody = SyncSessionEventsInputSchema.parse({ events });
30601
+ await postJson(`${apiUrl}/api/repos/${repoId}/events`, eventsBody, dryRun);
30602
+ }
30457
30603
  } catch (err) {
30458
30604
  const message = err instanceof Error ? err.message : String(err);
30459
30605
  process.stderr.write(`[har control] usage harvest skipped: ${message}
@@ -40151,18 +40297,20 @@ function printSignals() {
40151
40297
  async function handleStatus3(argv) {
40152
40298
  const preference = readTelemetryPreference();
40153
40299
  const enabled = isTelemetryEnabled();
40300
+ const signals = getTelemetrySignals();
40154
40301
  const apiUrl = getControlApiUrl();
40155
40302
  const reachable = enabled ? await isControlApiReachable(apiUrl) : false;
40156
40303
  const payload = {
40157
40304
  enabled,
40158
40305
  preferenceEnabled: preference.enabled,
40306
+ signals,
40159
40307
  preferencePath: getTelemetryPreferencePath(),
40160
40308
  updatedAt: preference.updatedAt ?? null,
40161
40309
  envOverride: process.env.HAR_TELEMETRY ?? null,
40162
40310
  controlApiUrl: apiUrl,
40163
40311
  controlReachable: reachable,
40164
40312
  otelEndpoint: `${apiUrl.replace(/\/$/, "")}/api/otel`,
40165
- signals: [...TELEMETRY_SIGNALS]
40313
+ signalDescriptions: [...TELEMETRY_SIGNALS]
40166
40314
  };
40167
40315
  if (argv.json) {
40168
40316
  process.stdout.write(`${JSON.stringify(payload, null, 2)}
@@ -40172,21 +40320,33 @@ async function handleStatus3(argv) {
40172
40320
  header("har telemetry status");
40173
40321
  info(`Enabled: ${enabled ? "yes" : "no"}${process.env.HAR_TELEMETRY ? " (HAR_TELEMETRY override)" : ""}`);
40174
40322
  info(`Preference: ${getTelemetryPreferencePath()}${preference.updatedAt ? ` (updated ${preference.updatedAt})` : " (default on)"}`);
40323
+ info(
40324
+ `Signals: metrics=${signals.metrics} logs=${signals.logs} prompts=${signals.prompts} traces=${signals.traces}`
40325
+ );
40175
40326
  info(`Mission Control: ${apiUrl} (${reachable ? "reachable" : "not reachable"})`);
40176
40327
  info(`OTLP endpoint: ${payload.otelEndpoint}`);
40177
40328
  printSignals();
40178
40329
  }
40179
- async function handleOn() {
40330
+ async function handleOn(argv) {
40180
40331
  header("har telemetry on");
40181
- writeTelemetryPreference(true);
40332
+ const preference = writeTelemetryPreference(true, {
40333
+ prompts: argv.prompts === true ? true : void 0,
40334
+ traces: argv.traces === true ? true : void 0
40335
+ });
40182
40336
  success("Telemetry enabled (opt-out). Agent usage will be stored in local Mission Control.");
40337
+ info(
40338
+ `Signals: metrics=${preference.signals.metrics} logs=${preference.signals.logs} prompts=${preference.signals.prompts} traces=${preference.signals.traces}`
40339
+ );
40340
+ if (preference.signals.prompts) {
40341
+ warn("Prompt/response text will leave the agent machine for local Mission Control storage.");
40342
+ }
40183
40343
  printSignals();
40184
40344
  const result = await ensureTelemetryInfrastructure({ startIfNeeded: true });
40185
40345
  if (result.message) success(result.message);
40186
40346
  if (result.warning) warn(result.warning);
40187
40347
  if (result.otelReady) {
40188
40348
  info(`OTLP ingest: ${result.apiUrl.replace(/\/$/, "")}/api/otel`);
40189
- info("Usage appears under Mission Control \u2192 Worktrees. Disable: har telemetry off");
40349
+ info("Usage appears under Mission Control \u2192 Worktrees / Usage. Disable: har telemetry off");
40190
40350
  }
40191
40351
  }
40192
40352
  async function handleOff() {
@@ -40296,10 +40456,20 @@ var telemetryCommand = {
40296
40456
  ).command(
40297
40457
  "on",
40298
40458
  "Enable telemetry (default) and ensure Mission Control is running",
40299
- () => {
40300
- },
40301
- () => {
40302
- void handleOn();
40459
+ (y2) => y2.option("prompts", {
40460
+ type: "boolean",
40461
+ default: false,
40462
+ describe: "Opt in to shipping user/assistant prompt text to Mission Control"
40463
+ }).option("traces", {
40464
+ type: "boolean",
40465
+ default: false,
40466
+ describe: "Opt in to thin OTEL traces ingest (Claude beta)"
40467
+ }),
40468
+ (argv) => {
40469
+ void handleOn({
40470
+ prompts: argv.prompts,
40471
+ traces: argv.traces
40472
+ });
40303
40473
  }
40304
40474
  ).command(
40305
40475
  "off",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osfactory/har",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "LLM-powered CLI for reproducible agent development environments",
5
5
  "keywords": [
6
6
  "har",