crewly 1.20.302 → 1.20.303

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 (157) hide show
  1. package/config/roles/developer/prompt.md +7 -0
  2. package/config/roles/fullstack-dev/prompt.md +7 -0
  3. package/config/roles/product-manager/prompt.md +7 -0
  4. package/config/skills/orchestrator/usage-categories/SKILL.md +59 -0
  5. package/config/skills/orchestrator/usage-categories/execute.sh +81 -0
  6. package/config/skills/orchestrator/usage-categories/execute.test.sh +45 -0
  7. package/config/skills/orchestrator/usage-report/SKILL.md +71 -0
  8. package/config/skills/orchestrator/usage-report/execute.sh +84 -0
  9. package/config/skills/orchestrator/usage-report/execute.test.sh +39 -0
  10. package/config/skills/team-leader/usage-report/SKILL.md +71 -0
  11. package/config/skills/team-leader/usage-report/execute.sh +84 -0
  12. package/config/skills/team-leader/usage-report/execute.test.sh +39 -0
  13. package/dist/backend/backend/src/constants.d.ts +11 -0
  14. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  15. package/dist/backend/backend/src/constants.js +11 -0
  16. package/dist/backend/backend/src/constants.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.d.ts +36 -0
  18. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.d.ts.map +1 -0
  19. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.js +158 -0
  20. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.js.map +1 -0
  21. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.d.ts +15 -0
  22. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.d.ts.map +1 -0
  23. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.js +26 -0
  24. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.js.map +1 -0
  25. package/dist/backend/backend/src/index.d.ts +10 -0
  26. package/dist/backend/backend/src/index.d.ts.map +1 -1
  27. package/dist/backend/backend/src/index.js +75 -17
  28. package/dist/backend/backend/src/index.js.map +1 -1
  29. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  30. package/dist/backend/backend/src/routes/api.routes.js +3 -0
  31. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  32. package/dist/backend/backend/src/services/agent/follow-through.wiring.d.ts +32 -0
  33. package/dist/backend/backend/src/services/agent/follow-through.wiring.d.ts.map +1 -1
  34. package/dist/backend/backend/src/services/agent/follow-through.wiring.js +42 -1
  35. package/dist/backend/backend/src/services/agent/follow-through.wiring.js.map +1 -1
  36. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts +26 -6
  37. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  38. package/dist/backend/backend/src/services/agent/resource-mode.service.js +59 -5
  39. package/dist/backend/backend/src/services/agent/resource-mode.service.js.map +1 -1
  40. package/dist/backend/backend/src/services/monitoring/agent-turn-state.d.ts +35 -0
  41. package/dist/backend/backend/src/services/monitoring/agent-turn-state.d.ts.map +1 -1
  42. package/dist/backend/backend/src/services/monitoring/agent-turn-state.js +101 -0
  43. package/dist/backend/backend/src/services/monitoring/agent-turn-state.js.map +1 -1
  44. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.d.ts +58 -0
  45. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.d.ts.map +1 -0
  46. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.js +100 -0
  47. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.js.map +1 -0
  48. package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
  49. package/dist/backend/backend/src/services/monitoring/token-usage.service.js +3 -0
  50. package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
  51. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts +7 -0
  52. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
  53. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +15 -0
  54. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
  55. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts +24 -0
  56. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
  57. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js +63 -1
  58. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
  59. package/dist/backend/backend/src/services/trace/trace-context.service.d.ts +42 -2
  60. package/dist/backend/backend/src/services/trace/trace-context.service.d.ts.map +1 -1
  61. package/dist/backend/backend/src/services/trace/trace-context.service.js +66 -5
  62. package/dist/backend/backend/src/services/trace/trace-context.service.js.map +1 -1
  63. package/dist/backend/backend/src/services/trace/trace-recorder.d.ts.map +1 -1
  64. package/dist/backend/backend/src/services/trace/trace-recorder.js +11 -0
  65. package/dist/backend/backend/src/services/trace/trace-recorder.js.map +1 -1
  66. package/dist/backend/backend/src/services/usage-ledger/categories-param.d.ts +28 -0
  67. package/dist/backend/backend/src/services/usage-ledger/categories-param.d.ts.map +1 -0
  68. package/dist/backend/backend/src/services/usage-ledger/categories-param.js +67 -0
  69. package/dist/backend/backend/src/services/usage-ledger/categories-param.js.map +1 -0
  70. package/dist/backend/backend/src/services/usage-ledger/excerpt.d.ts +37 -0
  71. package/dist/backend/backend/src/services/usage-ledger/excerpt.d.ts.map +1 -0
  72. package/dist/backend/backend/src/services/usage-ledger/excerpt.js +49 -0
  73. package/dist/backend/backend/src/services/usage-ledger/excerpt.js.map +1 -0
  74. package/dist/backend/backend/src/services/usage-ledger/index.d.ts +14 -0
  75. package/dist/backend/backend/src/services/usage-ledger/index.d.ts.map +1 -0
  76. package/dist/backend/backend/src/services/usage-ledger/index.js +14 -0
  77. package/dist/backend/backend/src/services/usage-ledger/index.js.map +1 -0
  78. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.d.ts +44 -0
  79. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.d.ts.map +1 -0
  80. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.js +52 -0
  81. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.js.map +1 -0
  82. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.d.ts +100 -0
  83. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.d.ts.map +1 -0
  84. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.js +189 -0
  85. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.js.map +1 -0
  86. package/dist/backend/backend/src/services/usage-ledger/owner-spans.d.ts +41 -0
  87. package/dist/backend/backend/src/services/usage-ledger/owner-spans.d.ts.map +1 -0
  88. package/dist/backend/backend/src/services/usage-ledger/owner-spans.js +71 -0
  89. package/dist/backend/backend/src/services/usage-ledger/owner-spans.js.map +1 -0
  90. package/dist/backend/backend/src/services/usage-ledger/team-directory.d.ts +47 -0
  91. package/dist/backend/backend/src/services/usage-ledger/team-directory.d.ts.map +1 -0
  92. package/dist/backend/backend/src/services/usage-ledger/team-directory.js +106 -0
  93. package/dist/backend/backend/src/services/usage-ledger/team-directory.js.map +1 -0
  94. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.d.ts +101 -0
  95. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.d.ts.map +1 -0
  96. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.js +263 -0
  97. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.js.map +1 -0
  98. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.d.ts +96 -0
  99. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.d.ts.map +1 -0
  100. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.js +166 -0
  101. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.js.map +1 -0
  102. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.d.ts +90 -0
  103. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.d.ts.map +1 -0
  104. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.js +294 -0
  105. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.js.map +1 -0
  106. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.d.ts +102 -0
  107. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.d.ts.map +1 -0
  108. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.js +314 -0
  109. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.js.map +1 -0
  110. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.d.ts +62 -0
  111. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.d.ts.map +1 -0
  112. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.js +106 -0
  113. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.js.map +1 -0
  114. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.d.ts +52 -0
  115. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.d.ts.map +1 -0
  116. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.js +95 -0
  117. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.js.map +1 -0
  118. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.d.ts +127 -0
  119. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.d.ts.map +1 -0
  120. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.js +19 -0
  121. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.js.map +1 -0
  122. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.d.ts +124 -0
  123. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.d.ts.map +1 -0
  124. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.js +372 -0
  125. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.js.map +1 -0
  126. package/dist/backend/build-info.json +2 -2
  127. package/dist/cli/backend/src/constants.d.ts +11 -0
  128. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  129. package/dist/cli/backend/src/constants.js +11 -0
  130. package/dist/cli/backend/src/constants.js.map +1 -1
  131. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts +26 -6
  132. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  133. package/dist/cli/backend/src/services/agent/resource-mode.service.js +59 -5
  134. package/dist/cli/backend/src/services/agent/resource-mode.service.js.map +1 -1
  135. package/dist/cli/backend/src/services/monitoring/agent-turn-state.d.ts +35 -0
  136. package/dist/cli/backend/src/services/monitoring/agent-turn-state.d.ts.map +1 -1
  137. package/dist/cli/backend/src/services/monitoring/agent-turn-state.js +101 -0
  138. package/dist/cli/backend/src/services/monitoring/agent-turn-state.js.map +1 -1
  139. package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
  140. package/dist/cli/backend/src/services/monitoring/token-usage.service.js +3 -0
  141. package/dist/cli/backend/src/services/monitoring/token-usage.service.js.map +1 -1
  142. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts +24 -0
  143. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
  144. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js +63 -1
  145. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
  146. package/dist/cli/backend/src/services/trace/trace-context.service.d.ts +42 -2
  147. package/dist/cli/backend/src/services/trace/trace-context.service.d.ts.map +1 -1
  148. package/dist/cli/backend/src/services/trace/trace-context.service.js +66 -5
  149. package/dist/cli/backend/src/services/trace/trace-context.service.js.map +1 -1
  150. package/dist/cli/backend/src/services/trace/trace-recorder.d.ts.map +1 -1
  151. package/dist/cli/backend/src/services/trace/trace-recorder.js +11 -0
  152. package/dist/cli/backend/src/services/trace/trace-recorder.js.map +1 -1
  153. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.d.ts +44 -0
  154. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.d.ts.map +1 -0
  155. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.js +52 -0
  156. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.js.map +1 -0
  157. package/package.json +1 -1
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Which team and project an agent session belongs to, read from the team
3
+ * configs under CREWLY_HOME (`teams/<id>/config.json`, `projects.json`).
4
+ * Cached; a stale cache is refreshed in the background so hot paths stay
5
+ * synchronous and cheap.
6
+ *
7
+ * @module services/usage-ledger/team-directory
8
+ */
9
+ import { promises as fs } from 'fs';
10
+ import * as path from 'path';
11
+ import { getCrewlyHomePath } from '../core/crewly-home.utils.js';
12
+ /** How long a loaded cache is trusted. */
13
+ const TTL_MS = 60_000;
14
+ /** The orchestrator's session is not in any team. */
15
+ const ORCHESTRATOR_SESSION = /^crewly-orc(?:$|-)/;
16
+ /**
17
+ * Reads team and project names from disk.
18
+ */
19
+ export class TeamDirectory {
20
+ home;
21
+ now;
22
+ sessions = new Map();
23
+ projects = new Map();
24
+ loadedAt = 0;
25
+ loading = null;
26
+ /**
27
+ * @param home - Crewly home (default CREWLY_HOME)
28
+ * @param now - Clock (ms)
29
+ */
30
+ constructor(home = getCrewlyHomePath(), now = () => Date.now()) {
31
+ this.home = home;
32
+ this.now = now;
33
+ }
34
+ lookup(session) {
35
+ if (this.now() - this.loadedAt > TTL_MS)
36
+ void this.refresh();
37
+ if (ORCHESTRATOR_SESSION.test(session))
38
+ return { team: 'Orchestrator' };
39
+ return this.sessions.get(session) ?? null;
40
+ }
41
+ projectName(projectId) {
42
+ return this.projects.get(projectId);
43
+ }
44
+ async ready() {
45
+ if (this.loadedAt === 0)
46
+ await this.refresh();
47
+ else if (this.loading)
48
+ await this.loading;
49
+ }
50
+ /**
51
+ * Reload the cache (one load at a time).
52
+ */
53
+ refresh() {
54
+ if (!this.loading) {
55
+ this.loading = this.load()
56
+ .catch(() => undefined)
57
+ .finally(() => {
58
+ this.loading = null;
59
+ });
60
+ }
61
+ return this.loading;
62
+ }
63
+ async load() {
64
+ const projects = new Map();
65
+ try {
66
+ const list = JSON.parse(await fs.readFile(path.join(this.home, 'projects.json'), 'utf-8'));
67
+ for (const p of Array.isArray(list) ? list : [])
68
+ if (p?.id && p.name)
69
+ projects.set(p.id, p.name);
70
+ }
71
+ catch {
72
+ // no projects file
73
+ }
74
+ const sessions = new Map();
75
+ try {
76
+ const teamsDir = path.join(this.home, 'teams');
77
+ for (const entry of await fs.readdir(teamsDir, { withFileTypes: true })) {
78
+ if (!entry.isDirectory())
79
+ continue;
80
+ try {
81
+ const team = JSON.parse(await fs.readFile(path.join(teamsDir, entry.name, 'config.json'), 'utf-8'));
82
+ if (!team?.name)
83
+ continue;
84
+ const project = team.projectIds?.map((id) => projects.get(id)).find((n) => !!n);
85
+ const info = { team: team.name, ...(project ? { project } : {}) };
86
+ for (const m of team.members ?? []) {
87
+ if (m.sessionName)
88
+ sessions.set(m.sessionName, info);
89
+ if (m.agentId)
90
+ sessions.set(m.agentId, info);
91
+ }
92
+ }
93
+ catch {
94
+ // unreadable team
95
+ }
96
+ }
97
+ }
98
+ catch {
99
+ // no teams dir
100
+ }
101
+ this.sessions = sessions;
102
+ this.projects = projects;
103
+ this.loadedAt = this.now();
104
+ }
105
+ }
106
+ //# sourceMappingURL=team-directory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team-directory.js","sourceRoot":"","sources":["../../../../../../backend/src/services/usage-ledger/team-directory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Puts tickets into categories at report time.
3
+ *
4
+ * Order of attempts, cheapest first:
5
+ * 1. Deterministic: keywords (or the comma-separated phrases of the
6
+ * description), team and project hints.
7
+ * 2. Only for tickets nothing matched, and only when a model is available: one
8
+ * small batched question to the default cheap model.
9
+ * 3. Otherwise `other`.
10
+ *
11
+ * With no definitions the category is the ticket's team (else its project).
12
+ *
13
+ * Results are cached per (category-set id, ticket); a fallback `other` that
14
+ * came from having no model is not cached, so it is retried later. The cache
15
+ * lives beside the ledger and is never part of the ticket facts.
16
+ *
17
+ * @module services/usage-ledger/unit-classifier
18
+ */
19
+ import { type CategoryDef } from './usage-categories.store.js';
20
+ /** Category of tickets that match nothing. */
21
+ export declare const OTHER_CATEGORY = "other";
22
+ /** Category of usage that belongs to no ticket. */
23
+ export declare const NO_TICKET_CATEGORY = "(no ticket)";
24
+ /** File with the classification cache. */
25
+ export declare const CLASSIFICATION_CACHE_FILE = "classification-cache.json";
26
+ /** What the classifier reads about a ticket. */
27
+ export interface ClassifiableUnit {
28
+ unitId: string;
29
+ ticketId?: string;
30
+ title?: string;
31
+ excerpt?: string;
32
+ team?: string;
33
+ project?: string;
34
+ }
35
+ /** Asks a small model one question; null when none is available or it failed. */
36
+ export type CategoryLlm = (prompt: string) => Promise<string | null>;
37
+ /** Where tickets ended up. */
38
+ export interface ClassifyResult {
39
+ /** Ticket → category */
40
+ categories: Map<string, string>;
41
+ /** Tickets answered by the model in this call */
42
+ llmClassified: number;
43
+ /** Tickets that matched nothing and had no model to ask */
44
+ unclassified: number;
45
+ }
46
+ /** Default classification (no user categories): the ticket's team, else its project. */
47
+ export declare function defaultCategory(unit: ClassifiableUnit): string;
48
+ /**
49
+ * Keywords of a definition: its explicit keywords, else the comma / semicolon
50
+ * separated phrases of its description (so "Xiaohongshu, Substack, LinkedIn"
51
+ * works as typed).
52
+ *
53
+ * @param def - Category definition
54
+ * @returns Lower-case keywords
55
+ */
56
+ export declare function keywordsOf(def: CategoryDef): string[];
57
+ /**
58
+ * Best category by keyword / team / project hints.
59
+ *
60
+ * @param unit - The ticket
61
+ * @param defs - Definitions
62
+ * @returns Category name, or null when nothing matched
63
+ */
64
+ export declare function matchCategory(unit: ClassifiableUnit, defs: CategoryDef[]): string | null;
65
+ /**
66
+ * Classifies tickets for reports.
67
+ */
68
+ export declare class UnitClassifier {
69
+ private readonly llm;
70
+ private readonly cacheFile;
71
+ private cache;
72
+ /**
73
+ * @param dir - Ledger directory
74
+ * @param llm - Optional model (null: deterministic only)
75
+ */
76
+ constructor(dir: string, llm?: CategoryLlm | null);
77
+ private loadCache;
78
+ /**
79
+ * Classify tickets.
80
+ *
81
+ * @param units - Tickets (never the "(no ticket)" pseudo-units)
82
+ * @param defs - The user's categories; empty means the default (team / project)
83
+ * @returns Category per ticket
84
+ */
85
+ classify(units: ClassifiableUnit[], defs: CategoryDef[]): Promise<ClassifyResult>;
86
+ private saveCache;
87
+ /**
88
+ * Ask the model about tickets nothing matched.
89
+ *
90
+ * @returns Ticket → category (only tickets the model answered validly)
91
+ */
92
+ private askModel;
93
+ }
94
+ /**
95
+ * The default cheap model: a small Anthropic model when `ANTHROPIC_API_KEY`
96
+ * is set; none otherwise.
97
+ *
98
+ * @returns A question function, or null
99
+ */
100
+ export declare function defaultCategoryLlm(): CategoryLlm | null;
101
+ //# sourceMappingURL=unit-classifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unit-classifier.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/usage-ledger/unit-classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAE9E,8CAA8C;AAC9C,eAAO,MAAM,cAAc,UAAU,CAAC;AACtC,mDAAmD;AACnD,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAEhD,0CAA0C;AAC1C,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AAQrE,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,iFAAiF;AACjF,MAAM,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAErE,8BAA8B;AAC9B,MAAM,WAAW,cAAc;IAC9B,wBAAwB;IACxB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,iDAAiD;IACjD,aAAa,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;CACrB;AAMD,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,EAAE,CAUrD;AAaD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,GAAG,IAAI,CAaxF;AAED;;GAEG;AACH,qBAAa,cAAc;IAUzB,OAAO,CAAC,QAAQ,CAAC,GAAG;IATrB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,KAAK,CAA0B;IAEvC;;;OAGG;gBAEF,GAAG,EAAE,MAAM,EACM,GAAG,GAAE,WAAW,GAAG,IAAW;YAKlC,SAAS;IAWvB;;;;;;OAMG;IACG,QAAQ,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC;YA2CzE,SAAS;IAcvB;;;;OAIG;YACW,QAAQ;CAsCtB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,WAAW,GAAG,IAAI,CAcvD"}
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Puts tickets into categories at report time.
3
+ *
4
+ * Order of attempts, cheapest first:
5
+ * 1. Deterministic: keywords (or the comma-separated phrases of the
6
+ * description), team and project hints.
7
+ * 2. Only for tickets nothing matched, and only when a model is available: one
8
+ * small batched question to the default cheap model.
9
+ * 3. Otherwise `other`.
10
+ *
11
+ * With no definitions the category is the ticket's team (else its project).
12
+ *
13
+ * Results are cached per (category-set id, ticket); a fallback `other` that
14
+ * came from having no model is not cached, so it is retried later. The cache
15
+ * lives beside the ledger and is never part of the ticket facts.
16
+ *
17
+ * @module services/usage-ledger/unit-classifier
18
+ */
19
+ import { promises as fs } from 'fs';
20
+ import * as path from 'path';
21
+ import { atomicWriteJson } from '../../utils/file-io.utils.js';
22
+ import { categorySetId } from './usage-categories.store.js';
23
+ /** Category of tickets that match nothing. */
24
+ export const OTHER_CATEGORY = 'other';
25
+ /** Category of usage that belongs to no ticket. */
26
+ export const NO_TICKET_CATEGORY = '(no ticket)';
27
+ /** File with the classification cache. */
28
+ export const CLASSIFICATION_CACHE_FILE = 'classification-cache.json';
29
+ /** Sets kept in the cache (oldest dropped). */
30
+ const MAX_CACHED_SETS = 6;
31
+ /** Tickets sent to the model in one call, and calls per report. */
32
+ const LLM_BATCH = 25;
33
+ const LLM_MAX_CALLS = 2;
34
+ /** Default classification (no user categories): the ticket's team, else its project. */
35
+ export function defaultCategory(unit) {
36
+ return unit.team?.trim() || unit.project?.trim() || OTHER_CATEGORY;
37
+ }
38
+ /**
39
+ * Keywords of a definition: its explicit keywords, else the comma / semicolon
40
+ * separated phrases of its description (so "Xiaohongshu, Substack, LinkedIn"
41
+ * works as typed).
42
+ *
43
+ * @param def - Category definition
44
+ * @returns Lower-case keywords
45
+ */
46
+ export function keywordsOf(def) {
47
+ const explicit = def.keywords ?? [];
48
+ const phrases = explicit.length > 0
49
+ ? []
50
+ : (def.description ?? '')
51
+ .split(/[,;,;、\n]+/)
52
+ .map((p) => p.trim())
53
+ .filter((p) => p.length >= 2 && p.length <= 40);
54
+ return [...new Set([...explicit, ...phrases].map((k) => k.toLowerCase()))];
55
+ }
56
+ /**
57
+ * Whether a lower-case text contains a keyword. Short ASCII keywords must
58
+ * match whole words ("ai" must not match "email").
59
+ */
60
+ function hasKeyword(text, keyword) {
61
+ if (/^[a-z0-9 ]+$/.test(keyword) && keyword.length <= 3) {
62
+ return new RegExp(`(^|[^a-z0-9])${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^a-z0-9]|$)`).test(text);
63
+ }
64
+ return text.includes(keyword);
65
+ }
66
+ /**
67
+ * Best category by keyword / team / project hints.
68
+ *
69
+ * @param unit - The ticket
70
+ * @param defs - Definitions
71
+ * @returns Category name, or null when nothing matched
72
+ */
73
+ export function matchCategory(unit, defs) {
74
+ const text = [unit.title, unit.excerpt, unit.ticketId, unit.team, unit.project].filter(Boolean).join(' \n ').toLowerCase();
75
+ const team = (unit.team ?? '').toLowerCase();
76
+ const project = (unit.project ?? '').toLowerCase();
77
+ let best = null;
78
+ for (const def of defs) {
79
+ let score = 0;
80
+ for (const k of keywordsOf(def))
81
+ if (hasKeyword(text, k))
82
+ score += 1;
83
+ if (team && def.teams?.some((t) => t.toLowerCase() === team))
84
+ score += 2;
85
+ if (project && def.projects?.some((p) => p.toLowerCase() === project))
86
+ score += 2;
87
+ if (score > 0 && (!best || score > best.score))
88
+ best = { name: def.name, score };
89
+ }
90
+ return best?.name ?? null;
91
+ }
92
+ /**
93
+ * Classifies tickets for reports.
94
+ */
95
+ export class UnitClassifier {
96
+ llm;
97
+ cacheFile;
98
+ cache = null;
99
+ /**
100
+ * @param dir - Ledger directory
101
+ * @param llm - Optional model (null: deterministic only)
102
+ */
103
+ constructor(dir, llm = null) {
104
+ this.llm = llm;
105
+ this.cacheFile = path.join(dir, CLASSIFICATION_CACHE_FILE);
106
+ }
107
+ async loadCache() {
108
+ if (this.cache)
109
+ return this.cache;
110
+ try {
111
+ const parsed = JSON.parse(await fs.readFile(this.cacheFile, 'utf-8'));
112
+ this.cache = parsed && typeof parsed.sets === 'object' && parsed.sets ? parsed : { sets: {} };
113
+ }
114
+ catch {
115
+ this.cache = { sets: {} };
116
+ }
117
+ return this.cache;
118
+ }
119
+ /**
120
+ * Classify tickets.
121
+ *
122
+ * @param units - Tickets (never the "(no ticket)" pseudo-units)
123
+ * @param defs - The user's categories; empty means the default (team / project)
124
+ * @returns Category per ticket
125
+ */
126
+ async classify(units, defs) {
127
+ const categories = new Map();
128
+ if (defs.length === 0) {
129
+ for (const u of units)
130
+ categories.set(u.unitId, defaultCategory(u));
131
+ return { categories, llmClassified: 0, unclassified: 0 };
132
+ }
133
+ const setId = categorySetId(defs);
134
+ const cache = await this.loadCache();
135
+ const cached = cache.sets[setId] ?? {};
136
+ const fresh = {};
137
+ const unmatched = [];
138
+ for (const u of units) {
139
+ const known = cached[u.unitId];
140
+ if (known) {
141
+ categories.set(u.unitId, known);
142
+ continue;
143
+ }
144
+ const hit = matchCategory(u, defs);
145
+ if (hit) {
146
+ categories.set(u.unitId, hit);
147
+ fresh[u.unitId] = hit;
148
+ }
149
+ else {
150
+ unmatched.push(u);
151
+ }
152
+ }
153
+ let llmClassified = 0;
154
+ let unclassified = 0;
155
+ const answers = this.llm ? await this.askModel(unmatched, defs) : new Map();
156
+ for (const u of unmatched) {
157
+ const answer = answers.get(u.unitId);
158
+ if (answer) {
159
+ categories.set(u.unitId, answer);
160
+ fresh[u.unitId] = answer;
161
+ if (answer !== OTHER_CATEGORY)
162
+ llmClassified += 1;
163
+ }
164
+ else {
165
+ categories.set(u.unitId, OTHER_CATEGORY);
166
+ unclassified += 1;
167
+ }
168
+ }
169
+ if (Object.keys(fresh).length > 0)
170
+ await this.saveCache(setId, fresh);
171
+ return { categories, llmClassified, unclassified };
172
+ }
173
+ async saveCache(setId, fresh) {
174
+ const cache = await this.loadCache();
175
+ const merged = { ...(cache.sets[setId] ?? {}), ...fresh };
176
+ delete cache.sets[setId];
177
+ cache.sets[setId] = merged;
178
+ for (const key of Object.keys(cache.sets).slice(0, Math.max(0, Object.keys(cache.sets).length - MAX_CACHED_SETS)))
179
+ delete cache.sets[key];
180
+ try {
181
+ await fs.mkdir(path.dirname(this.cacheFile), { recursive: true });
182
+ await atomicWriteJson(this.cacheFile, cache);
183
+ }
184
+ catch {
185
+ // the cache is an optimisation
186
+ }
187
+ }
188
+ /**
189
+ * Ask the model about tickets nothing matched.
190
+ *
191
+ * @returns Ticket → category (only tickets the model answered validly)
192
+ */
193
+ async askModel(units, defs) {
194
+ const out = new Map();
195
+ if (!this.llm || units.length === 0)
196
+ return out;
197
+ const names = new Map(defs.map((d) => [d.name.toLowerCase(), d.name]));
198
+ const catalogue = defs.map((d) => `- ${d.name}${d.description ? `: ${d.description}` : ''}`).join('\n');
199
+ for (let call = 0; call < LLM_MAX_CALLS; call++) {
200
+ const batch = units.slice(call * LLM_BATCH, (call + 1) * LLM_BATCH);
201
+ if (batch.length === 0)
202
+ break;
203
+ const list = batch
204
+ .map((u, i) => `${i + 1}. ${[u.title, u.excerpt && u.excerpt !== u.title ? u.excerpt.slice(0, 160) : '', u.team ? `(team ${u.team})` : ''].filter(Boolean).join(' | ')}`)
205
+ .join('\n');
206
+ const prompt = `Assign each work item to exactly one category, or "${OTHER_CATEGORY}" if none fits.\n\nCategories:\n${catalogue}\n\nWork items:\n${list}\n\n` +
207
+ `Reply with only a JSON object mapping the item number to the category name, for example {"1":"${defs[0].name}","2":"${OTHER_CATEGORY}"}.`;
208
+ let reply = null;
209
+ try {
210
+ reply = await this.llm(prompt);
211
+ }
212
+ catch {
213
+ reply = null;
214
+ }
215
+ if (!reply)
216
+ break;
217
+ const json = /\{[\s\S]*\}/.exec(reply)?.[0];
218
+ if (!json)
219
+ continue;
220
+ try {
221
+ const parsed = JSON.parse(json);
222
+ batch.forEach((u, i) => {
223
+ const value = parsed[String(i + 1)];
224
+ if (typeof value !== 'string')
225
+ return;
226
+ const name = value.trim().toLowerCase();
227
+ if (name === OTHER_CATEGORY)
228
+ out.set(u.unitId, OTHER_CATEGORY);
229
+ else if (names.has(name))
230
+ out.set(u.unitId, names.get(name));
231
+ });
232
+ }
233
+ catch {
234
+ // unparseable answer: those tickets stay unclassified
235
+ }
236
+ }
237
+ return out;
238
+ }
239
+ }
240
+ /**
241
+ * The default cheap model: a small Anthropic model when `ANTHROPIC_API_KEY`
242
+ * is set; none otherwise.
243
+ *
244
+ * @returns A question function, or null
245
+ */
246
+ export function defaultCategoryLlm() {
247
+ if (!process.env.ANTHROPIC_API_KEY)
248
+ return null;
249
+ return async (prompt) => {
250
+ const { generateText } = await import('ai');
251
+ const { anthropic } = await import('@ai-sdk/anthropic');
252
+ const controller = new AbortController();
253
+ const timer = setTimeout(() => controller.abort(), 30_000);
254
+ try {
255
+ const result = await generateText({ model: anthropic('claude-haiku-4-5'), prompt, abortSignal: controller.signal, maxOutputTokens: 600 });
256
+ return result.text;
257
+ }
258
+ finally {
259
+ clearTimeout(timer);
260
+ }
261
+ };
262
+ }
263
+ //# sourceMappingURL=unit-classifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unit-classifier.js","sourceRoot":"","sources":["../../../../../../backend/src/services/usage-ledger/unit-classifier.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The owner's own categories for usage reports ("self-media", "book", …).
3
+ *
4
+ * Categories are optional and never written into ticket facts: they only
5
+ * decide how a report groups tickets. Each definition has a natural-language
6
+ * description and optional hints (keywords, team names, project names). The
7
+ * set carries a version that changes whenever a definition changes; the
8
+ * classification cache is keyed on it.
9
+ *
10
+ * @module services/usage-ledger/usage-categories.store
11
+ */
12
+ /** File holding the definitions. */
13
+ export declare const CATEGORIES_FILE = "categories.json";
14
+ /** One user-defined category. */
15
+ export interface CategoryDef {
16
+ /** Short label shown in reports */
17
+ name: string;
18
+ /** What belongs here, in plain words (also read by the language model) */
19
+ description?: string;
20
+ /** Words or phrases that put a ticket here (case-insensitive) */
21
+ keywords?: string[];
22
+ /** Team names that belong here */
23
+ teams?: string[];
24
+ /** Project names that belong here */
25
+ projects?: string[];
26
+ }
27
+ /**
28
+ * Validate and normalise a definition.
29
+ *
30
+ * @param input - Candidate
31
+ * @returns The definition, or an error text
32
+ */
33
+ export declare function normalizeCategory(input: unknown): CategoryDef | string;
34
+ /**
35
+ * Identity of a set of definitions: the same definitions always give the same
36
+ * id, so a classification cache survives a set being re-entered unchanged.
37
+ *
38
+ * @param defs - Definitions
39
+ * @returns `default` for none, else a short hash
40
+ */
41
+ export declare function categorySetId(defs: CategoryDef[]): string;
42
+ /**
43
+ * File-backed category definitions.
44
+ */
45
+ export declare class UsageCategoriesStore {
46
+ readonly dir: string;
47
+ private readonly file;
48
+ /**
49
+ * @param dir - Ledger directory (default `~/.crewly/usage-ledger`)
50
+ */
51
+ constructor(dir?: string);
52
+ private read;
53
+ private write;
54
+ /**
55
+ * The current definitions.
56
+ *
57
+ * @returns Version, set id and definitions
58
+ */
59
+ list(): Promise<{
60
+ version: number;
61
+ setId: string;
62
+ updatedAt: string;
63
+ categories: CategoryDef[];
64
+ }>;
65
+ /**
66
+ * Add or replace one category (matched by name, case-insensitive).
67
+ *
68
+ * @param input - The definition
69
+ * @returns The new state, or an error text
70
+ */
71
+ set(input: unknown): Promise<{
72
+ version: number;
73
+ categories: CategoryDef[];
74
+ } | string>;
75
+ /**
76
+ * Replace every definition.
77
+ *
78
+ * @param inputs - The new set (empty clears it)
79
+ * @returns The new state, or an error text
80
+ */
81
+ replaceAll(inputs: unknown[]): Promise<{
82
+ version: number;
83
+ categories: CategoryDef[];
84
+ } | string>;
85
+ /**
86
+ * Remove one category.
87
+ *
88
+ * @param name - Its name (case-insensitive)
89
+ * @returns The new state, or null when there was no such category
90
+ */
91
+ remove(name: string): Promise<{
92
+ version: number;
93
+ categories: CategoryDef[];
94
+ } | null>;
95
+ }
96
+ //# sourceMappingURL=usage-categories.store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-categories.store.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/usage-ledger/usage-categories.store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AASH,oCAAoC;AACpC,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAEjD,iCAAiC;AACjC,MAAM,WAAW,WAAW;IAC3B,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AA0BD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,MAAM,CAiBtE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,CAIzD;AAED;;GAEG;AACH,qBAAa,oBAAoB;IAMpB,QAAQ,CAAC,GAAG,EAAE,MAAM;IALhC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAE9B;;OAEG;gBACkB,GAAG,GAAE,MAAwD;YAIpE,IAAI;YAYJ,KAAK;IAOnB;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,WAAW,EAAE,CAAA;KAAE,CAAC;IAKvG;;;;;OAKG;IACG,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,WAAW,EAAE,CAAA;KAAE,GAAG,MAAM,CAAC;IAU3F;;;;;OAKG;IACG,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,WAAW,EAAE,CAAA;KAAE,GAAG,MAAM,CAAC;IAYrG;;;;;OAKG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,WAAW,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC;CAO1F"}