@ramarivera/coding-agent-langfuse 0.1.48 → 0.1.51

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/README.md CHANGED
@@ -54,14 +54,14 @@ real network cause, and preserves local state so reruns resume cleanly.
54
54
  ## Project tags and metadata
55
55
 
56
56
  Backfills and live followers can attach stable Langfuse dimensions based on the
57
- session cwd. Use this for client/workstream dashboards like ITSynch without
58
- guessing from host names or Windows paths in the Langfuse UI.
57
+ session cwd. Use this for client/workstream dashboards without guessing from
58
+ host names or Windows paths in the Langfuse UI.
59
59
 
60
60
  There are two config layers:
61
61
 
62
62
  - Global host config: `~/.config/coding-agent-langfuse/path-tags.json`
63
- - Optional project-local overlay: `.langfuse-ca.json` in the project directory
64
- or any parent of the session cwd
63
+ - Optional project-local overlays: every `.langfuse-ca.json` found while
64
+ walking from the session cwd up to the scanned home directory
65
65
 
66
66
  Global config uses prefix rules:
67
67
 
@@ -69,21 +69,23 @@ Global config uses prefix rules:
69
69
  {
70
70
  "rules": [
71
71
  {
72
- "pathPrefix": "/Users/ramarivera/dev/itsynch",
73
- "tags": ["itsynch", "client:itsynch"],
72
+ "pathPrefix": "/Users/example/dev/acme",
73
+ "tags": ["acme", "client:acme"],
74
74
  "metadata": {
75
- "project_group": "itsynch",
76
- "project_owner": "ramiro"
75
+ "project_group": "acme",
76
+ "project_owner": "platform-team"
77
77
  },
78
- "projectName": "itsynch",
79
- "projectFolder": "itsynch"
78
+ "projectName": "acme",
79
+ "projectFolder": "acme"
80
80
  }
81
81
  ]
82
82
  }
83
83
  ```
84
84
 
85
85
  Project-local config is intentionally smaller and overrides/extends the matched
86
- global rule:
86
+ global rule. Parent overlays are applied before child overlays, so a config at
87
+ `~/work/client-a/.langfuse-ca.json` can tag a whole workstream while nested
88
+ repos add repo-specific fields:
87
89
 
88
90
  ```json
89
91
  {
@@ -91,7 +93,7 @@ global rule:
91
93
  "metadata": {
92
94
  "service": "portal"
93
95
  },
94
- "projectName": "itsynch-portal"
96
+ "projectName": "acme-portal"
95
97
  }
96
98
  ```
97
99
 
@@ -112,9 +114,9 @@ npx @ramarivera/coding-agent-langfuse@latest \
112
114
  --path-tags-config "$HOME/.config/coding-agent-langfuse/path-tags.json"
113
115
  ```
114
116
 
115
- Generated services pass `--path-tags-config` when provided to
116
- `service install`; otherwise the package reads the default global config path if
117
- it exists.
117
+ Generated services keep their command small and rely on the package's default
118
+ global config plus upward project-local discovery. Use `--path-tags-config` only
119
+ for one-off CLI runs that need a non-default global config path.
118
120
 
119
121
  ## Cost calculation
120
122
 
@@ -67,6 +67,7 @@ type OtlpOptions = {
67
67
  maxFieldBytes?: number;
68
68
  costRates?: CostCatalog;
69
69
  pathTagsConfig?: PathTagsConfig;
70
+ homeDir?: string;
70
71
  };
71
72
  type PathTagRule = {
72
73
  pathPrefix: string;
package/dist/backfill.js CHANGED
@@ -196,7 +196,7 @@ function projectMetadata(cwd) {
196
196
  projectFolder: projectName,
197
197
  };
198
198
  }
199
- function matchPathTags(cwd, config) {
199
+ function matchPathTags(cwd, config, homeDir) {
200
200
  if (!cwd)
201
201
  return { tags: [], metadata: {} };
202
202
  const normalizedCwd = normalizeRulePath(cwd);
@@ -218,7 +218,7 @@ function matchPathTags(cwd, config) {
218
218
  const overlays = [
219
219
  globalMatch,
220
220
  config.project,
221
- loadProjectLocalOverlay(cwd),
221
+ ...loadProjectLocalOverlays(cwd, homeDir),
222
222
  ].filter((overlay) => overlay !== undefined);
223
223
  return overlays.reduce((acc, overlay) => {
224
224
  if (!overlay)
@@ -236,9 +236,9 @@ function matchPathTags(cwd, config) {
236
236
  projectFolder: undefined,
237
237
  });
238
238
  }
239
- function mergeProjectMetadata(cwd, config) {
239
+ function mergeProjectMetadata(cwd, config, homeDir) {
240
240
  const project = projectMetadata(cwd);
241
- const pathTags = matchPathTags(cwd, config);
241
+ const pathTags = matchPathTags(cwd, config, homeDir);
242
242
  return {
243
243
  ...project,
244
244
  projectName: pathTags.projectName ?? project.projectName,
@@ -528,10 +528,12 @@ function loadPathTagsConfig(path) {
528
528
  });
529
529
  return { rules, project: normalizeProjectTagOverlay(root, path) };
530
530
  }
531
- function loadProjectLocalOverlay(cwd) {
532
- const configPath = findProjectLocalConfig(cwd);
533
- if (!configPath)
534
- return undefined;
531
+ function loadProjectLocalOverlays(cwd, homeDir) {
532
+ return findProjectLocalConfigs(cwd, homeDir)
533
+ .map((configPath) => loadProjectLocalOverlay(configPath))
534
+ .filter((overlay) => overlay !== undefined);
535
+ }
536
+ function loadProjectLocalOverlay(configPath) {
535
537
  const cached = projectLocalConfigCache.get(configPath);
536
538
  if (projectLocalConfigCache.has(configPath))
537
539
  return cached;
@@ -545,31 +547,26 @@ function loadProjectLocalOverlay(cwd) {
545
547
  projectLocalConfigCache.set(configPath, overlay);
546
548
  return overlay;
547
549
  }
548
- function findProjectLocalConfig(cwd) {
549
- if (!cwd)
550
- return undefined;
551
- let current = cwd;
552
- while (true) {
553
- try {
554
- current = statSync(current).isDirectory() ? current : dirname(current);
555
- break;
556
- }
557
- catch {
558
- const parent = dirname(current);
559
- if (parent === current)
560
- return undefined;
561
- current = parent;
562
- }
563
- }
550
+ function findProjectLocalConfigs(cwd, homeDir) {
551
+ const start = nearestExistingDirectory(cwd);
552
+ if (!start)
553
+ return [];
554
+ const home = homeDir ? nearestExistingDirectory(homeDir) ?? homeDir : homedir();
555
+ const stopAtHome = isSameOrChildPath(start, home);
556
+ const configPaths = [];
557
+ let current = start;
564
558
  while (true) {
565
559
  const candidate = join(current, projectLocalConfigFile);
566
560
  if (existsSync(candidate))
567
- return candidate;
561
+ configPaths.push(candidate);
562
+ if (stopAtHome && sameNormalizedPath(current, home))
563
+ break;
568
564
  const parent = dirname(current);
569
565
  if (parent === current)
570
- return undefined;
566
+ break;
571
567
  current = parent;
572
568
  }
569
+ return configPaths.reverse();
573
570
  }
574
571
  function normalizeProjectTagOverlay(value, sourcePath) {
575
572
  const root = asRecord(value);
@@ -596,6 +593,15 @@ function normalizeRulePath(path) {
596
593
  const normalized = path.replace(/\\/g, "/").replace(/\/+$/, "");
597
594
  return /^[a-z]:\//i.test(normalized) ? normalized.toLowerCase() : normalized;
598
595
  }
596
+ function sameNormalizedPath(left, right) {
597
+ return normalizeRulePath(left) === normalizeRulePath(right);
598
+ }
599
+ function isSameOrChildPath(child, parent) {
600
+ const normalizedChild = normalizeRulePath(child);
601
+ const normalizedParent = normalizeRulePath(parent);
602
+ return normalizedChild === normalizedParent ||
603
+ normalizedChild.startsWith(`${normalizedParent}/`);
604
+ }
599
605
  function loadCostCatalogFromEnv() {
600
606
  let catalog = { ...defaultCostRates };
601
607
  const path = process.env.CODING_AGENT_LANGFUSE_COST_RATES_PATH ??
@@ -1868,6 +1874,7 @@ function toOtlp(events, options = {}) {
1868
1874
  const maxFieldBytes = options.maxFieldBytes ?? defaultMaxFieldBytes;
1869
1875
  const costRates = options.costRates ?? loadCostCatalogFromEnv();
1870
1876
  const pathTagsConfig = options.pathTagsConfig ?? { rules: [] };
1877
+ const projectConfigHomeDir = options.homeDir ?? process.env.HOME ?? homedir();
1871
1878
  const spansByTrace = new Map();
1872
1879
  for (const rawEvent of events) {
1873
1880
  const event = limitEventPayload(rawEvent, maxFieldBytes);
@@ -1882,7 +1889,7 @@ function toOtlp(events, options = {}) {
1882
1889
  const traceStartMs = sortedEvents[0]?.startMs ?? Date.now();
1883
1890
  const traceEndMs = Math.max(...sortedEvents.map((event) => event.endMs ?? event.startMs + 1), traceStartMs + 1);
1884
1891
  const shouldEmitRootSpan = sortedEvents.some((event) => event.recordId === "session");
1885
- const firstProject = mergeProjectMetadata(first.cwd, pathTagsConfig);
1892
+ const firstProject = mergeProjectMetadata(first.cwd, pathTagsConfig, projectConfigHomeDir);
1886
1893
  const rootAttributes = [
1887
1894
  attr("service.name", `agent.${first.agent}`),
1888
1895
  attr("deployment.environment", "local"),
@@ -1962,7 +1969,7 @@ function toOtlp(events, options = {}) {
1962
1969
  const generation = isGenerationEvent(event);
1963
1970
  const usage = usageDetails(event.usage);
1964
1971
  const cost = generation ? calculateCost(event, usage, costRates) : undefined;
1965
- const eventProject = mergeProjectMetadata(event.cwd, pathTagsConfig);
1972
+ const eventProject = mergeProjectMetadata(event.cwd, pathTagsConfig, projectConfigHomeDir);
1966
1973
  const attributes = [
1967
1974
  attr("service.name", `agent.${event.agent}`),
1968
1975
  attr("deployment.environment", "local"),
@@ -2205,6 +2212,7 @@ async function run(options) {
2205
2212
  maxFieldBytes: options.maxFieldBytes,
2206
2213
  costRates: options.costRates,
2207
2214
  pathTagsConfig: options.pathTagsConfig,
2215
+ homeDir: options.homeDir,
2208
2216
  });
2209
2217
  }
2210
2218
  catch (error) {
@@ -2232,6 +2240,7 @@ async function run(options) {
2232
2240
  maxFieldBytes: options.maxFieldBytes,
2233
2241
  costRates: options.costRates,
2234
2242
  pathTagsConfig: options.pathTagsConfig,
2243
+ homeDir: options.homeDir,
2235
2244
  auth: options.auth,
2236
2245
  });
2237
2246
  for (const event of batch) {
package/dist/service.js CHANGED
@@ -36,11 +36,11 @@ Service options:
36
36
  `;
37
37
  }
38
38
  function parseServiceArgs(argv) {
39
- const action = parseServiceAction(argv[0]);
40
39
  if (argv.includes("--help") || argv.includes("-h")) {
41
40
  console.log(serviceUsage());
42
41
  process.exit(0);
43
42
  }
43
+ const action = parseServiceAction(argv[0]);
44
44
  let platform = currentServicePlatform();
45
45
  let agents = [...allAgents];
46
46
  let endpoint = process.env.LANGFUSE_BACKFILL_ENDPOINT ?? defaultEndpoint;
@@ -56,8 +56,7 @@ function parseServiceArgs(argv) {
56
56
  process.env.LANGFUSE_BACKFILL_COST_RATES_PATH;
57
57
  let costRatesJson = process.env.CODING_AGENT_LANGFUSE_COST_RATES_JSON ??
58
58
  process.env.LANGFUSE_BACKFILL_COST_RATES_JSON;
59
- let pathTagsConfigPath = process.env.CODING_AGENT_LANGFUSE_PATH_TAGS_CONFIG ??
60
- process.env.LANGFUSE_BACKFILL_PATH_TAGS_CONFIG;
59
+ let pathTagsConfigPath;
61
60
  let since;
62
61
  let dryRun = false;
63
62
  let start = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.48",
3
+ "version": "0.1.51",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",