@ramarivera/coding-agent-langfuse 0.1.46 → 0.1.48

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
@@ -51,6 +51,71 @@ npx @ramarivera/coding-agent-langfuse@latest \
51
51
  The importer is fail-fast: the first failed OTLP POST stops the run, prints the
52
52
  real network cause, and preserves local state so reruns resume cleanly.
53
53
 
54
+ ## Project tags and metadata
55
+
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.
59
+
60
+ There are two config layers:
61
+
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
65
+
66
+ Global config uses prefix rules:
67
+
68
+ ```json
69
+ {
70
+ "rules": [
71
+ {
72
+ "pathPrefix": "/Users/ramarivera/dev/itsynch",
73
+ "tags": ["itsynch", "client:itsynch"],
74
+ "metadata": {
75
+ "project_group": "itsynch",
76
+ "project_owner": "ramiro"
77
+ },
78
+ "projectName": "itsynch",
79
+ "projectFolder": "itsynch"
80
+ }
81
+ ]
82
+ }
83
+ ```
84
+
85
+ Project-local config is intentionally smaller and overrides/extends the matched
86
+ global rule:
87
+
88
+ ```json
89
+ {
90
+ "tags": ["repo:portal"],
91
+ "metadata": {
92
+ "service": "portal"
93
+ },
94
+ "projectName": "itsynch-portal"
95
+ }
96
+ ```
97
+
98
+ Matched values are emitted on root trace metadata, observation metadata, and
99
+ top-level OTLP attributes such as `project.tags`, `project.group`, and
100
+ `project.owner`. When the session cwd is inside a Git repository, the exporter
101
+ also emits `git.worktree.path`, `git.branch`, and `git.commit` plus matching
102
+ Langfuse metadata fields. The root span also sets `langfuse.trace.tags`, so
103
+ Langfuse custom dashboards can filter/group by tags, metadata, branch, or
104
+ worktree. Historical repair runs use the same OTLP builder as live follow mode,
105
+ so rerunning with `--force` can backfill newly added tags and Git metadata for
106
+ existing sessions.
107
+
108
+ Use a non-default global config path with:
109
+
110
+ ```sh
111
+ npx @ramarivera/coding-agent-langfuse@latest \
112
+ --path-tags-config "$HOME/.config/coding-agent-langfuse/path-tags.json"
113
+ ```
114
+
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.
118
+
54
119
  ## Cost calculation
55
120
 
56
121
  Backfill cost calculation follows Langfuse's OpenTelemetry mapping: generation
@@ -50,6 +50,8 @@ type BackfillOptions = {
50
50
  maxFieldBytes: number;
51
51
  postDelayMs: number;
52
52
  costRates: CostCatalog;
53
+ pathTagsConfigPath?: string;
54
+ pathTagsConfig: PathTagsConfig;
53
55
  };
54
56
  type CostRates = {
55
57
  input?: number;
@@ -64,6 +66,25 @@ type CostCatalog = Record<string, CostRates>;
64
66
  type OtlpOptions = {
65
67
  maxFieldBytes?: number;
66
68
  costRates?: CostCatalog;
69
+ pathTagsConfig?: PathTagsConfig;
70
+ };
71
+ type PathTagRule = {
72
+ pathPrefix: string;
73
+ tags: string[];
74
+ metadata: Record<string, unknown>;
75
+ projectName?: string;
76
+ projectFolder?: string;
77
+ };
78
+ type PathTagsConfig = {
79
+ rules: PathTagRule[];
80
+ project?: ProjectTagOverlay;
81
+ };
82
+ type ProjectTagOverlay = {
83
+ tags: string[];
84
+ metadata: Record<string, unknown>;
85
+ projectName?: string;
86
+ projectFolder?: string;
87
+ sourcePath?: string;
67
88
  };
68
89
  type RunSummary = {
69
90
  discovered: Record<string, number>;
package/dist/backfill.js CHANGED
@@ -31,7 +31,11 @@ const deadRemoteEndpoint = "http://langfuse.ai.roxasroot.net:14318/v1/traces";
31
31
  const defaultMaxRequestBytes = 12 * 1024 * 1024;
32
32
  const defaultMaxFieldBytes = 512 * 1024;
33
33
  const defaultStatePath = join(homedir(), ".local/state/coding-agent-langfuse/backfill-v6.json");
34
+ const defaultPathTagsConfigPath = join(homedir(), ".config/coding-agent-langfuse/path-tags.json");
34
35
  const currentHost = hostname();
36
+ const projectLocalConfigFile = ".langfuse-ca.json";
37
+ const projectLocalConfigCache = new Map();
38
+ const gitMetadataCache = new Map();
35
39
  const kimiFirepassRates = {
36
40
  input: 2,
37
41
  output: 8,
@@ -192,6 +196,109 @@ function projectMetadata(cwd) {
192
196
  projectFolder: projectName,
193
197
  };
194
198
  }
199
+ function matchPathTags(cwd, config) {
200
+ if (!cwd)
201
+ return { tags: [], metadata: {} };
202
+ const normalizedCwd = normalizeRulePath(cwd);
203
+ const matched = config.rules.filter((rule) => {
204
+ const prefix = normalizeRulePath(rule.pathPrefix);
205
+ return normalizedCwd === prefix || normalizedCwd.startsWith(`${prefix}/`);
206
+ });
207
+ const globalMatch = matched.reduce((acc, rule) => ({
208
+ tags: [...new Set([...acc.tags, ...rule.tags])],
209
+ metadata: { ...acc.metadata, ...rule.metadata },
210
+ projectName: rule.projectName ?? acc.projectName,
211
+ projectFolder: rule.projectFolder ?? acc.projectFolder,
212
+ }), {
213
+ tags: [],
214
+ metadata: {},
215
+ projectName: undefined,
216
+ projectFolder: undefined,
217
+ });
218
+ const overlays = [
219
+ globalMatch,
220
+ config.project,
221
+ loadProjectLocalOverlay(cwd),
222
+ ].filter((overlay) => overlay !== undefined);
223
+ return overlays.reduce((acc, overlay) => {
224
+ if (!overlay)
225
+ return acc;
226
+ return {
227
+ tags: [...new Set([...acc.tags, ...overlay.tags])],
228
+ metadata: { ...acc.metadata, ...overlay.metadata },
229
+ projectName: overlay.projectName ?? acc.projectName,
230
+ projectFolder: overlay.projectFolder ?? acc.projectFolder,
231
+ };
232
+ }, {
233
+ tags: [],
234
+ metadata: {},
235
+ projectName: undefined,
236
+ projectFolder: undefined,
237
+ });
238
+ }
239
+ function mergeProjectMetadata(cwd, config) {
240
+ const project = projectMetadata(cwd);
241
+ const pathTags = matchPathTags(cwd, config);
242
+ return {
243
+ ...project,
244
+ projectName: pathTags.projectName ?? project.projectName,
245
+ projectFolder: pathTags.projectFolder ?? project.projectFolder,
246
+ tags: pathTags.tags,
247
+ metadata: pathTags.metadata,
248
+ git: loadGitMetadata(cwd),
249
+ };
250
+ }
251
+ function loadGitMetadata(cwd) {
252
+ const gitCwd = nearestExistingDirectory(cwd);
253
+ if (!gitCwd)
254
+ return {};
255
+ const cached = gitMetadataCache.get(gitCwd);
256
+ if (cached)
257
+ return cached;
258
+ const worktreePath = gitOutput(gitCwd, ["rev-parse", "--show-toplevel"]);
259
+ if (!worktreePath) {
260
+ gitMetadataCache.set(gitCwd, {});
261
+ return {};
262
+ }
263
+ const branch = gitOutput(gitCwd, ["branch", "--show-current"]) ??
264
+ gitOutput(gitCwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
265
+ const commit = gitOutput(gitCwd, ["rev-parse", "--verify", "HEAD"]);
266
+ const metadata = {
267
+ worktreePath,
268
+ branch: branch === "HEAD" ? undefined : branch,
269
+ commit,
270
+ };
271
+ gitMetadataCache.set(gitCwd, metadata);
272
+ return metadata;
273
+ }
274
+ function gitOutput(cwd, args) {
275
+ try {
276
+ const output = execFileSync("git", ["-C", cwd, ...args], {
277
+ encoding: "utf8",
278
+ stdio: ["ignore", "pipe", "ignore"],
279
+ }).trim();
280
+ return output.length > 0 ? output : undefined;
281
+ }
282
+ catch {
283
+ return undefined;
284
+ }
285
+ }
286
+ function nearestExistingDirectory(path) {
287
+ if (!path)
288
+ return undefined;
289
+ let current = path;
290
+ while (true) {
291
+ try {
292
+ return statSync(current).isDirectory() ? current : dirname(current);
293
+ }
294
+ catch {
295
+ const parent = dirname(current);
296
+ if (parent === current)
297
+ return undefined;
298
+ current = parent;
299
+ }
300
+ }
301
+ }
195
302
  function usage() {
196
303
  return `Usage: coding-agent-langfuse-backfill [options]
197
304
 
@@ -211,6 +318,7 @@ Options:
211
318
  --post-delay-ms N Delay after each successful OTLP POST (default: 0)
212
319
  --cost-rates PATH JSON model cost-rate overrides in USD per 1M tokens
213
320
  --cost-rates-json JSON Inline JSON model cost-rate overrides in USD per 1M tokens
321
+ --path-tags-config PATH JSON path-prefix rules that add Langfuse tags/metadata
214
322
  --follow Keep scanning and sending newly written events
215
323
  --poll-interval-ms N Delay between --follow scans (default: 5000)
216
324
  --idle-exit-after-ms N Stop --follow after this much time without new sends
@@ -241,6 +349,9 @@ function parseArgs(argv) {
241
349
  if (!Number.isFinite(postDelayMs))
242
350
  postDelayMs = 0;
243
351
  let costRates = loadCostCatalogFromEnv();
352
+ let pathTagsConfigPath = process.env.CODING_AGENT_LANGFUSE_PATH_TAGS_CONFIG ??
353
+ process.env.LANGFUSE_BACKFILL_PATH_TAGS_CONFIG ??
354
+ defaultPathTagsConfigPath;
244
355
  let follow = false;
245
356
  let pollIntervalMs = 5_000;
246
357
  let idleExitAfterMs;
@@ -307,6 +418,9 @@ function parseArgs(argv) {
307
418
  else if (arg === "--cost-rates-json") {
308
419
  costRates = mergeCostCatalog(costRates, parseCostCatalogJson(next()));
309
420
  }
421
+ else if (arg === "--path-tags-config") {
422
+ pathTagsConfigPath = next();
423
+ }
310
424
  else if (arg === "--follow") {
311
425
  follow = true;
312
426
  }
@@ -378,8 +492,110 @@ function parseArgs(argv) {
378
492
  maxFieldBytes,
379
493
  postDelayMs,
380
494
  costRates,
495
+ pathTagsConfigPath,
496
+ pathTagsConfig: loadPathTagsConfig(pathTagsConfigPath),
497
+ };
498
+ }
499
+ function loadPathTagsConfig(path) {
500
+ if (!path || !existsSync(path))
501
+ return { rules: [] };
502
+ let parsed;
503
+ try {
504
+ parsed = JSON.parse(readFileSync(path, "utf8"));
505
+ }
506
+ catch (error) {
507
+ throw new Error(`Invalid path tags config ${path}: ${describeError(error)}`);
508
+ }
509
+ const root = asRecord(parsed);
510
+ const rawRules = Array.isArray(root.rules) ? root.rules : [];
511
+ const rules = rawRules.map((rawRule, index) => {
512
+ const rule = asRecord(rawRule);
513
+ const pathPrefix = asString(rule.pathPrefix) ?? asString(rule.path_prefix);
514
+ if (!pathPrefix) {
515
+ throw new Error(`Invalid path tags config ${path}: rules[${index}].pathPrefix is required`);
516
+ }
517
+ const tags = Array.isArray(rule.tags)
518
+ ? rule.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0)
519
+ : [];
520
+ const metadata = asRecord(rule.metadata);
521
+ return {
522
+ pathPrefix,
523
+ tags,
524
+ metadata,
525
+ projectName: asString(rule.projectName) ?? asString(rule.project_name),
526
+ projectFolder: asString(rule.projectFolder) ?? asString(rule.project_folder),
527
+ };
528
+ });
529
+ return { rules, project: normalizeProjectTagOverlay(root, path) };
530
+ }
531
+ function loadProjectLocalOverlay(cwd) {
532
+ const configPath = findProjectLocalConfig(cwd);
533
+ if (!configPath)
534
+ return undefined;
535
+ const cached = projectLocalConfigCache.get(configPath);
536
+ if (projectLocalConfigCache.has(configPath))
537
+ return cached;
538
+ let overlay;
539
+ try {
540
+ overlay = normalizeProjectTagOverlay(JSON.parse(readFileSync(configPath, "utf8")), configPath);
541
+ }
542
+ catch (error) {
543
+ throw new Error(`Invalid project Langfuse config ${configPath}: ${describeError(error)}`);
544
+ }
545
+ projectLocalConfigCache.set(configPath, overlay);
546
+ return overlay;
547
+ }
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
+ }
564
+ while (true) {
565
+ const candidate = join(current, projectLocalConfigFile);
566
+ if (existsSync(candidate))
567
+ return candidate;
568
+ const parent = dirname(current);
569
+ if (parent === current)
570
+ return undefined;
571
+ current = parent;
572
+ }
573
+ }
574
+ function normalizeProjectTagOverlay(value, sourcePath) {
575
+ const root = asRecord(value);
576
+ const rawTags = Array.isArray(root.tags) ? root.tags : [];
577
+ const tags = rawTags.filter((tag) => typeof tag === "string" && tag.trim().length > 0);
578
+ const metadata = asRecord(root.metadata);
579
+ const projectName = asString(root.projectName) ?? asString(root.project_name);
580
+ const projectFolder = asString(root.projectFolder) ?? asString(root.project_folder);
581
+ if (tags.length === 0 &&
582
+ Object.keys(metadata).length === 0 &&
583
+ !projectName &&
584
+ !projectFolder) {
585
+ return undefined;
586
+ }
587
+ return {
588
+ tags,
589
+ metadata,
590
+ projectName,
591
+ projectFolder,
592
+ sourcePath,
381
593
  };
382
594
  }
595
+ function normalizeRulePath(path) {
596
+ const normalized = path.replace(/\\/g, "/").replace(/\/+$/, "");
597
+ return /^[a-z]:\//i.test(normalized) ? normalized.toLowerCase() : normalized;
598
+ }
383
599
  function loadCostCatalogFromEnv() {
384
600
  let catalog = { ...defaultCostRates };
385
601
  const path = process.env.CODING_AGENT_LANGFUSE_COST_RATES_PATH ??
@@ -1613,6 +1829,12 @@ function attr(key, value) {
1613
1829
  return { key, value: { stringValue: value } };
1614
1830
  return { key, value: { stringValue: JSON.stringify(value).slice(0, 8000) } };
1615
1831
  }
1832
+ function nonEmptyArray(value) {
1833
+ return value.length > 0 ? value : undefined;
1834
+ }
1835
+ function nonEmptyRecord(value) {
1836
+ return Object.keys(value).length > 0 ? value : undefined;
1837
+ }
1616
1838
  function utf8Bytes(value) {
1617
1839
  return Buffer.byteLength(value, "utf8");
1618
1840
  }
@@ -1645,6 +1867,7 @@ function limitEventPayload(event, maxFieldBytes) {
1645
1867
  function toOtlp(events, options = {}) {
1646
1868
  const maxFieldBytes = options.maxFieldBytes ?? defaultMaxFieldBytes;
1647
1869
  const costRates = options.costRates ?? loadCostCatalogFromEnv();
1870
+ const pathTagsConfig = options.pathTagsConfig ?? { rules: [] };
1648
1871
  const spansByTrace = new Map();
1649
1872
  for (const rawEvent of events) {
1650
1873
  const event = limitEventPayload(rawEvent, maxFieldBytes);
@@ -1659,7 +1882,7 @@ function toOtlp(events, options = {}) {
1659
1882
  const traceStartMs = sortedEvents[0]?.startMs ?? Date.now();
1660
1883
  const traceEndMs = Math.max(...sortedEvents.map((event) => event.endMs ?? event.startMs + 1), traceStartMs + 1);
1661
1884
  const shouldEmitRootSpan = sortedEvents.some((event) => event.recordId === "session");
1662
- const firstProject = projectMetadata(first.cwd);
1885
+ const firstProject = mergeProjectMetadata(first.cwd, pathTagsConfig);
1663
1886
  const rootAttributes = [
1664
1887
  attr("service.name", `agent.${first.agent}`),
1665
1888
  attr("deployment.environment", "local"),
@@ -1682,6 +1905,12 @@ function toOtlp(events, options = {}) {
1682
1905
  attr("langfuse.trace.metadata.project_path", firstProject.projectPath),
1683
1906
  attr("langfuse.trace.metadata.project_name", firstProject.projectName),
1684
1907
  attr("langfuse.trace.metadata.project_folder", firstProject.projectFolder),
1908
+ attr("langfuse.trace.metadata.project_tags", nonEmptyArray(firstProject.tags)),
1909
+ attr("langfuse.trace.metadata.path_tags", nonEmptyRecord(firstProject.metadata)),
1910
+ attr("langfuse.trace.metadata.git_worktree_path", firstProject.git.worktreePath),
1911
+ attr("langfuse.trace.metadata.git_branch", firstProject.git.branch),
1912
+ attr("langfuse.trace.metadata.git_commit", firstProject.git.commit),
1913
+ attr("langfuse.trace.tags", nonEmptyArray(firstProject.tags)),
1685
1914
  attr("langfuse.trace.metadata.import_payload_version", payloadVersion(first)),
1686
1915
  attr("langfuse.trace.metadata.import_state_identity", importIdentity(first)),
1687
1916
  attr("langfuse.trace.metadata.langfuse_id_identity", langfuseIdIdentity(first)),
@@ -1695,6 +1924,11 @@ function toOtlp(events, options = {}) {
1695
1924
  attr("langfuse.observation.metadata.project_path", firstProject.projectPath),
1696
1925
  attr("langfuse.observation.metadata.project_name", firstProject.projectName),
1697
1926
  attr("langfuse.observation.metadata.project_folder", firstProject.projectFolder),
1927
+ attr("langfuse.observation.metadata.project_tags", nonEmptyArray(firstProject.tags)),
1928
+ attr("langfuse.observation.metadata.path_tags", nonEmptyRecord(firstProject.metadata)),
1929
+ attr("langfuse.observation.metadata.git_worktree_path", firstProject.git.worktreePath),
1930
+ attr("langfuse.observation.metadata.git_branch", firstProject.git.branch),
1931
+ attr("langfuse.observation.metadata.git_commit", firstProject.git.commit),
1698
1932
  attr("langfuse.observation.metadata.import_payload_version", payloadVersion(first)),
1699
1933
  attr("langfuse.observation.metadata.import_state_identity", importIdentity(first)),
1700
1934
  attr("langfuse.observation.metadata.langfuse_id_identity", langfuseIdIdentity(first)),
@@ -1703,6 +1937,12 @@ function toOtlp(events, options = {}) {
1703
1937
  attr("project.path", firstProject.projectPath),
1704
1938
  attr("project.name", firstProject.projectName),
1705
1939
  attr("project.folder", firstProject.projectFolder),
1940
+ attr("project.tags", nonEmptyArray(firstProject.tags)),
1941
+ attr("project.group", firstProject.metadata.project_group),
1942
+ attr("project.owner", firstProject.metadata.project_owner),
1943
+ attr("git.worktree.path", firstProject.git.worktreePath),
1944
+ attr("git.branch", firstProject.git.branch),
1945
+ attr("git.commit", firstProject.git.commit),
1706
1946
  ].filter((item) => Boolean(item));
1707
1947
  const rootSpan = {
1708
1948
  traceId: traceId(first),
@@ -1722,7 +1962,7 @@ function toOtlp(events, options = {}) {
1722
1962
  const generation = isGenerationEvent(event);
1723
1963
  const usage = usageDetails(event.usage);
1724
1964
  const cost = generation ? calculateCost(event, usage, costRates) : undefined;
1725
- const eventProject = projectMetadata(event.cwd);
1965
+ const eventProject = mergeProjectMetadata(event.cwd, pathTagsConfig);
1726
1966
  const attributes = [
1727
1967
  attr("service.name", `agent.${event.agent}`),
1728
1968
  attr("deployment.environment", "local"),
@@ -1747,6 +1987,11 @@ function toOtlp(events, options = {}) {
1747
1987
  attr("langfuse.observation.metadata.project_path", eventProject.projectPath),
1748
1988
  attr("langfuse.observation.metadata.project_name", eventProject.projectName),
1749
1989
  attr("langfuse.observation.metadata.project_folder", eventProject.projectFolder),
1990
+ attr("langfuse.observation.metadata.project_tags", nonEmptyArray(eventProject.tags)),
1991
+ attr("langfuse.observation.metadata.path_tags", nonEmptyRecord(eventProject.metadata)),
1992
+ attr("langfuse.observation.metadata.git_worktree_path", eventProject.git.worktreePath),
1993
+ attr("langfuse.observation.metadata.git_branch", eventProject.git.branch),
1994
+ attr("langfuse.observation.metadata.git_commit", eventProject.git.commit),
1750
1995
  attr("langfuse.observation.metadata.model", modelName ?? event.model),
1751
1996
  attr("langfuse.observation.metadata.provider", event.provider),
1752
1997
  attr("langfuse.observation.metadata.import_payload_version", payloadVersion(event)),
@@ -1770,6 +2015,12 @@ function toOtlp(events, options = {}) {
1770
2015
  attr("project.path", eventProject.projectPath),
1771
2016
  attr("project.name", eventProject.projectName),
1772
2017
  attr("project.folder", eventProject.projectFolder),
2018
+ attr("project.tags", nonEmptyArray(eventProject.tags)),
2019
+ attr("project.group", eventProject.metadata.project_group),
2020
+ attr("project.owner", eventProject.metadata.project_owner),
2021
+ attr("git.worktree.path", eventProject.git.worktreePath),
2022
+ attr("git.branch", eventProject.git.branch),
2023
+ attr("git.commit", eventProject.git.commit),
1773
2024
  attr("role", event.role),
1774
2025
  attr("agent.model", event.model),
1775
2026
  attr("agent.provider", event.provider),
@@ -1953,6 +2204,7 @@ async function run(options) {
1953
2204
  maxRequestBytes: options.maxRequestBytes,
1954
2205
  maxFieldBytes: options.maxFieldBytes,
1955
2206
  costRates: options.costRates,
2207
+ pathTagsConfig: options.pathTagsConfig,
1956
2208
  });
1957
2209
  }
1958
2210
  catch (error) {
@@ -1979,6 +2231,7 @@ async function run(options) {
1979
2231
  await postOtlp(options.endpoint, batch, {
1980
2232
  maxFieldBytes: options.maxFieldBytes,
1981
2233
  costRates: options.costRates,
2234
+ pathTagsConfig: options.pathTagsConfig,
1982
2235
  auth: options.auth,
1983
2236
  });
1984
2237
  for (const event of batch) {
package/dist/service.d.ts CHANGED
@@ -15,6 +15,7 @@ type ServiceOptions = {
15
15
  postDelayMs: number;
16
16
  costRatesPath?: string;
17
17
  costRatesJson?: string;
18
+ pathTagsConfigPath?: string;
18
19
  npxPath: string;
19
20
  since?: string;
20
21
  dryRun: boolean;
package/dist/service.js CHANGED
@@ -26,6 +26,7 @@ Service options:
26
26
  --post-delay-ms N Delay after each successful OTLP POST (default: 0)
27
27
  --cost-rates PATH JSON model cost-rate overrides in USD per 1M tokens
28
28
  --cost-rates-json JSON Inline JSON model cost-rate overrides in USD per 1M tokens
29
+ --path-tags-config PATH JSON path-prefix rules that add Langfuse tags/metadata
29
30
  --since ISO_OR_MS Optional lower bound for events the follower may send
30
31
  --working-directory DIR Directory the service starts in (default: --home)
31
32
  --path VALUE PATH value injected into the service environment
@@ -55,6 +56,8 @@ function parseServiceArgs(argv) {
55
56
  process.env.LANGFUSE_BACKFILL_COST_RATES_PATH;
56
57
  let costRatesJson = process.env.CODING_AGENT_LANGFUSE_COST_RATES_JSON ??
57
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;
58
61
  let since;
59
62
  let dryRun = false;
60
63
  let start = true;
@@ -107,6 +110,9 @@ function parseServiceArgs(argv) {
107
110
  else if (arg === "--cost-rates-json") {
108
111
  costRatesJson = next();
109
112
  }
113
+ else if (arg === "--path-tags-config") {
114
+ pathTagsConfigPath = next();
115
+ }
110
116
  else if (arg === "--since") {
111
117
  since = next();
112
118
  }
@@ -148,6 +154,7 @@ function parseServiceArgs(argv) {
148
154
  postDelayMs,
149
155
  costRatesPath,
150
156
  costRatesJson,
157
+ pathTagsConfigPath,
151
158
  since,
152
159
  dryRun,
153
160
  start,
@@ -300,6 +307,8 @@ function buildFollowCommand(options) {
300
307
  command.push("--cost-rates", options.costRatesPath);
301
308
  if (options.costRatesJson)
302
309
  command.push("--cost-rates-json", options.costRatesJson);
310
+ if (options.pathTagsConfigPath)
311
+ command.push("--path-tags-config", options.pathTagsConfigPath);
303
312
  return command;
304
313
  }
305
314
  function renderSystemdUnit(options, command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",