crewly 1.20.301 → 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 (203) 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/agent/core/receive-file/SKILL.md +4 -0
  5. package/config/skills/agent/core/request-file/SKILL.md +74 -0
  6. package/config/skills/agent/core/request-file/execute.sh +89 -0
  7. package/config/skills/agent/core/request-file/execute.test.sh +77 -0
  8. package/config/skills/agent/core/slack-file/SKILL.md +1 -1
  9. package/config/skills/orchestrator/usage-categories/SKILL.md +59 -0
  10. package/config/skills/orchestrator/usage-categories/execute.sh +81 -0
  11. package/config/skills/orchestrator/usage-categories/execute.test.sh +45 -0
  12. package/config/skills/orchestrator/usage-report/SKILL.md +71 -0
  13. package/config/skills/orchestrator/usage-report/execute.sh +84 -0
  14. package/config/skills/orchestrator/usage-report/execute.test.sh +39 -0
  15. package/config/skills/team-leader/usage-report/SKILL.md +71 -0
  16. package/config/skills/team-leader/usage-report/execute.sh +84 -0
  17. package/config/skills/team-leader/usage-report/execute.test.sh +39 -0
  18. package/dist/backend/backend/src/constants.d.ts +24 -4
  19. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  20. package/dist/backend/backend/src/constants.js +29 -6
  21. package/dist/backend/backend/src/constants.js.map +1 -1
  22. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +9 -0
  23. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
  24. package/dist/backend/backend/src/controllers/apps/apps.controller.js +21 -0
  25. package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
  26. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +2 -0
  27. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts.map +1 -1
  28. package/dist/backend/backend/src/controllers/apps/apps.routes.js +7 -1
  29. package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
  30. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.d.ts +36 -0
  31. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.d.ts.map +1 -0
  32. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.js +158 -0
  33. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.js.map +1 -0
  34. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.d.ts +15 -0
  35. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.d.ts.map +1 -0
  36. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.js +26 -0
  37. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.js.map +1 -0
  38. package/dist/backend/backend/src/index.d.ts +10 -0
  39. package/dist/backend/backend/src/index.d.ts.map +1 -1
  40. package/dist/backend/backend/src/index.js +75 -17
  41. package/dist/backend/backend/src/index.js.map +1 -1
  42. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  43. package/dist/backend/backend/src/routes/api.routes.js +3 -0
  44. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  45. package/dist/backend/backend/src/services/agent/follow-through.wiring.d.ts +32 -0
  46. package/dist/backend/backend/src/services/agent/follow-through.wiring.d.ts.map +1 -1
  47. package/dist/backend/backend/src/services/agent/follow-through.wiring.js +42 -1
  48. package/dist/backend/backend/src/services/agent/follow-through.wiring.js.map +1 -1
  49. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts +26 -6
  50. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  51. package/dist/backend/backend/src/services/agent/resource-mode.service.js +59 -5
  52. package/dist/backend/backend/src/services/agent/resource-mode.service.js.map +1 -1
  53. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  54. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  55. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  56. package/dist/backend/backend/src/services/apps/apps.wiring.d.ts.map +1 -1
  57. package/dist/backend/backend/src/services/apps/apps.wiring.js +14 -0
  58. package/dist/backend/backend/src/services/apps/apps.wiring.js.map +1 -1
  59. package/dist/backend/backend/src/services/apps/file-request-watch.service.d.ts +88 -0
  60. package/dist/backend/backend/src/services/apps/file-request-watch.service.d.ts.map +1 -0
  61. package/dist/backend/backend/src/services/apps/file-request-watch.service.js +170 -0
  62. package/dist/backend/backend/src/services/apps/file-request-watch.service.js.map +1 -0
  63. package/dist/backend/backend/src/services/monitoring/agent-turn-state.d.ts +35 -0
  64. package/dist/backend/backend/src/services/monitoring/agent-turn-state.d.ts.map +1 -1
  65. package/dist/backend/backend/src/services/monitoring/agent-turn-state.js +101 -0
  66. package/dist/backend/backend/src/services/monitoring/agent-turn-state.js.map +1 -1
  67. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.d.ts +58 -0
  68. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.d.ts.map +1 -0
  69. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.js +100 -0
  70. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.js.map +1 -0
  71. package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
  72. package/dist/backend/backend/src/services/monitoring/token-usage.service.js +3 -0
  73. package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
  74. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts +7 -0
  75. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
  76. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +15 -0
  77. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
  78. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +51 -33
  80. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  81. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts +24 -0
  82. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js +63 -1
  84. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
  85. package/dist/backend/backend/src/services/trace/trace-context.service.d.ts +42 -2
  86. package/dist/backend/backend/src/services/trace/trace-context.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/trace/trace-context.service.js +66 -5
  88. package/dist/backend/backend/src/services/trace/trace-context.service.js.map +1 -1
  89. package/dist/backend/backend/src/services/trace/trace-recorder.d.ts.map +1 -1
  90. package/dist/backend/backend/src/services/trace/trace-recorder.js +11 -0
  91. package/dist/backend/backend/src/services/trace/trace-recorder.js.map +1 -1
  92. package/dist/backend/backend/src/services/usage-ledger/categories-param.d.ts +28 -0
  93. package/dist/backend/backend/src/services/usage-ledger/categories-param.d.ts.map +1 -0
  94. package/dist/backend/backend/src/services/usage-ledger/categories-param.js +67 -0
  95. package/dist/backend/backend/src/services/usage-ledger/categories-param.js.map +1 -0
  96. package/dist/backend/backend/src/services/usage-ledger/excerpt.d.ts +37 -0
  97. package/dist/backend/backend/src/services/usage-ledger/excerpt.d.ts.map +1 -0
  98. package/dist/backend/backend/src/services/usage-ledger/excerpt.js +49 -0
  99. package/dist/backend/backend/src/services/usage-ledger/excerpt.js.map +1 -0
  100. package/dist/backend/backend/src/services/usage-ledger/index.d.ts +14 -0
  101. package/dist/backend/backend/src/services/usage-ledger/index.d.ts.map +1 -0
  102. package/dist/backend/backend/src/services/usage-ledger/index.js +14 -0
  103. package/dist/backend/backend/src/services/usage-ledger/index.js.map +1 -0
  104. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.d.ts +44 -0
  105. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.d.ts.map +1 -0
  106. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.js +52 -0
  107. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.js.map +1 -0
  108. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.d.ts +100 -0
  109. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.d.ts.map +1 -0
  110. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.js +189 -0
  111. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.js.map +1 -0
  112. package/dist/backend/backend/src/services/usage-ledger/owner-spans.d.ts +41 -0
  113. package/dist/backend/backend/src/services/usage-ledger/owner-spans.d.ts.map +1 -0
  114. package/dist/backend/backend/src/services/usage-ledger/owner-spans.js +71 -0
  115. package/dist/backend/backend/src/services/usage-ledger/owner-spans.js.map +1 -0
  116. package/dist/backend/backend/src/services/usage-ledger/team-directory.d.ts +47 -0
  117. package/dist/backend/backend/src/services/usage-ledger/team-directory.d.ts.map +1 -0
  118. package/dist/backend/backend/src/services/usage-ledger/team-directory.js +106 -0
  119. package/dist/backend/backend/src/services/usage-ledger/team-directory.js.map +1 -0
  120. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.d.ts +101 -0
  121. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.d.ts.map +1 -0
  122. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.js +263 -0
  123. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.js.map +1 -0
  124. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.d.ts +96 -0
  125. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.d.ts.map +1 -0
  126. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.js +166 -0
  127. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.js.map +1 -0
  128. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.d.ts +90 -0
  129. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.d.ts.map +1 -0
  130. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.js +294 -0
  131. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.js.map +1 -0
  132. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.d.ts +102 -0
  133. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.d.ts.map +1 -0
  134. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.js +314 -0
  135. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.js.map +1 -0
  136. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.d.ts +62 -0
  137. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.d.ts.map +1 -0
  138. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.js +106 -0
  139. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.js.map +1 -0
  140. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.d.ts +52 -0
  141. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.d.ts.map +1 -0
  142. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.js +95 -0
  143. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.js.map +1 -0
  144. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.d.ts +127 -0
  145. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.d.ts.map +1 -0
  146. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.js +19 -0
  147. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.js.map +1 -0
  148. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.d.ts +124 -0
  149. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.d.ts.map +1 -0
  150. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.js +372 -0
  151. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.js.map +1 -0
  152. package/dist/backend/backend/src/types/slack.types.d.ts +8 -0
  153. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  154. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  155. package/dist/backend/build-info.json +2 -2
  156. package/dist/cli/backend/src/constants.d.ts +24 -4
  157. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  158. package/dist/cli/backend/src/constants.js +29 -6
  159. package/dist/cli/backend/src/constants.js.map +1 -1
  160. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts +26 -6
  161. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  162. package/dist/cli/backend/src/services/agent/resource-mode.service.js +59 -5
  163. package/dist/cli/backend/src/services/agent/resource-mode.service.js.map +1 -1
  164. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  165. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  166. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  167. package/dist/cli/backend/src/services/monitoring/agent-turn-state.d.ts +35 -0
  168. package/dist/cli/backend/src/services/monitoring/agent-turn-state.d.ts.map +1 -1
  169. package/dist/cli/backend/src/services/monitoring/agent-turn-state.js +101 -0
  170. package/dist/cli/backend/src/services/monitoring/agent-turn-state.js.map +1 -1
  171. package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
  172. package/dist/cli/backend/src/services/monitoring/token-usage.service.js +3 -0
  173. package/dist/cli/backend/src/services/monitoring/token-usage.service.js.map +1 -1
  174. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  175. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +51 -33
  176. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  177. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts +24 -0
  178. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
  179. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js +63 -1
  180. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
  181. package/dist/cli/backend/src/services/trace/trace-context.service.d.ts +42 -2
  182. package/dist/cli/backend/src/services/trace/trace-context.service.d.ts.map +1 -1
  183. package/dist/cli/backend/src/services/trace/trace-context.service.js +66 -5
  184. package/dist/cli/backend/src/services/trace/trace-context.service.js.map +1 -1
  185. package/dist/cli/backend/src/services/trace/trace-recorder.d.ts.map +1 -1
  186. package/dist/cli/backend/src/services/trace/trace-recorder.js +11 -0
  187. package/dist/cli/backend/src/services/trace/trace-recorder.js.map +1 -1
  188. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.d.ts +44 -0
  189. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.d.ts.map +1 -0
  190. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.js +52 -0
  191. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.js.map +1 -0
  192. package/dist/cli/backend/src/services/worktree/low-disk-guard.d.ts +109 -0
  193. package/dist/cli/backend/src/services/worktree/low-disk-guard.d.ts.map +1 -0
  194. package/dist/cli/backend/src/services/worktree/low-disk-guard.js +148 -0
  195. package/dist/cli/backend/src/services/worktree/low-disk-guard.js.map +1 -0
  196. package/dist/cli/backend/src/services/worktree/worktree-janitor.git.d.ts +224 -0
  197. package/dist/cli/backend/src/services/worktree/worktree-janitor.git.d.ts.map +1 -0
  198. package/dist/cli/backend/src/services/worktree/worktree-janitor.git.js +449 -0
  199. package/dist/cli/backend/src/services/worktree/worktree-janitor.git.js.map +1 -0
  200. package/dist/cli/backend/src/types/slack.types.d.ts +8 -0
  201. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  202. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  203. package/package.json +1 -1
@@ -406,6 +406,13 @@ Default tier: **Fast Path** (greenfield, internal tools, prototypes). Escalate t
406
406
  - The Owner is consulted only for goal change, scope change, customer-facing commitment, irreversible expense, or strategic direction.
407
407
 
408
408
 
409
+ ## Long Builds and Background Forks
410
+
411
+ - Do long multi-step builds (rendering, video, large refactors) in the foreground of your own turn. That is the default.
412
+ - A background subagent or fork can die silently, and then nobody delivers what you promised. If you do use one, keep its description specific, tell the owner only what you can verify, and check on it: look at its output or transcript yourself within a few minutes, and do not end the turn on "it will report back" without a plan for when you look.
413
+ - Crewly tells you when a background task made no progress for about 10 minutes (`[BACKGROUND TASK STALLED]`). When you see it, inspect what it produced and continue the work yourself in the foreground.
414
+ - A promise to the owner ("starting now, will post here") stays yours until the result is posted, whoever does the work.
415
+
409
416
  ## Lazy Behavior Anti-Patterns
410
417
 
411
418
  You are failing the task if you:
@@ -186,6 +186,13 @@ Default tier: **Fast Path** (greenfield, internal tools, prototypes). Escalate t
186
186
  - The Owner is consulted only for goal change, scope change, customer-facing commitment, irreversible expense, or strategic direction.
187
187
 
188
188
 
189
+ ## Long Builds and Background Forks
190
+
191
+ - Do long multi-step builds (rendering, video, large refactors) in the foreground of your own turn. That is the default.
192
+ - A background subagent or fork can die silently, and then nobody delivers what you promised. If you do use one, keep its description specific, tell the owner only what you can verify, and check on it: look at its output or transcript yourself within a few minutes, and do not end the turn on "it will report back" without a plan for when you look.
193
+ - Crewly tells you when a background task made no progress for about 10 minutes (`[BACKGROUND TASK STALLED]`). When you see it, inspect what it produced and continue the work yourself in the foreground.
194
+ - A promise to the owner ("starting now, will post here") stays yours until the result is posted, whoever does the work.
195
+
189
196
  ## Lazy Behavior Anti-Patterns
190
197
 
191
198
  You are failing the task if you:
@@ -296,6 +296,13 @@ Default tier: **Standard Path** (customer-facing or coordination work). Drop to
296
296
  - The Owner is consulted only for goal change, scope change, customer-facing commitment, irreversible expense, or strategic direction.
297
297
 
298
298
 
299
+ ## Long Builds and Background Forks
300
+
301
+ - Do long multi-step builds (rendering, video, large refactors) in the foreground of your own turn. That is the default.
302
+ - A background subagent or fork can die silently, and then nobody delivers what you promised. If you do use one, keep its description specific, tell the owner only what you can verify, and check on it: look at its output or transcript yourself within a few minutes, and do not end the turn on "it will report back" without a plan for when you look.
303
+ - Crewly tells you when a background task made no progress for about 10 minutes (`[BACKGROUND TASK STALLED]`). When you see it, inspect what it produced and continue the work yourself in the foreground.
304
+ - A promise to the owner ("starting now, will post here") stays yours until the result is posted, whoever does the work.
305
+
299
306
  ## Lazy Behavior Anti-Patterns
300
307
 
301
308
  You are failing the task if you:
@@ -49,5 +49,9 @@ Prints:
49
49
  24 hours pass).
50
50
  - An interrupted download keeps its `.part` file and resumes on the next run.
51
51
  - `--no-ack` keeps the server copy so another run can fetch it again.
52
+ - Also how an **owner's upload** reaches you: when the owner uploads a file for a
53
+ link you made with `request-file`, you get a message with the handoff id and this
54
+ command (file kept up to 7 days). If the owner has a file that is too large for
55
+ Slack, use `request-file` instead of asking them to compress it or use Drive.
52
56
  - A handoff is gone once received, cancelled or expired (24 h): ask the sender
53
57
  to send it again.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: Request File
3
+ description: Ask the OWNER for a file that is too big for Slack (video, archive, anything over 1 GB or not on Slack) by creating a private upload link they open on their phone. The upload lands on this machine and you are told to receive it. Use instead of asking the owner to compress it or put it in Drive.
4
+ version: 1.0.0
5
+ category: productivity
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - "*"
9
+ triggers:
10
+ - request file
11
+ - upload link
12
+ - file too large
13
+ - could not be downloaded
14
+ - the video is too big
15
+ - get the file from the owner
16
+ - need the footage
17
+ tags:
18
+ - files
19
+ - upload
20
+ - owner
21
+ execution:
22
+ type: script
23
+ script:
24
+ file: execute.sh
25
+ interpreter: bash
26
+ timeoutMs: 60000
27
+ ---
28
+
29
+ # Request File
30
+
31
+ Gets a big file from **the owner**, who is probably on a phone and away from the machine.
32
+
33
+ ```bash
34
+ bash execute.sh --purpose "the raw footage for the launch video"
35
+ bash execute.sh --purpose "the signed contract (PDF)" --max-files 1
36
+ bash execute.sh --status ur_AbCdEfGhIjKlMnOpQrStUv
37
+ bash execute.sh --cancel ur_AbCdEfGhIjKlMnOpQrStUv
38
+ ```
39
+
40
+ Prints:
41
+
42
+ ```json
43
+ {"success":true,"requestId":"ur_…","url":"https://crewlyai.com/portal/upload/ur_…","expiresAt":"…",
44
+ "limits":{"maxFileBytes":2147483648,"maxFiles":10},"instruction":"Post the link to the owner in your own words…"}
45
+ ```
46
+
47
+ ## What to do with it
48
+
49
+ 1. **Post the `url` to the owner** where you are talking to them (the Slack
50
+ thread, the chat), in your own words: what you need, why, and that they open
51
+ the link on their phone, sign in if asked, and pick the file from the camera
52
+ roll. One short message. Do not paste this skill's JSON.
53
+ 2. **Carry on with anything else you can do.** You do not need to wait or poll:
54
+ when the upload finishes you get a message with the file name and the exact
55
+ `receive-file` command. Run it, then tell the owner in your own words that you
56
+ have it.
57
+ 3. If the owner says it does not work or the link expired (48 hours), run the
58
+ skill again for a fresh link. `--cancel` withdraws a link you no longer need.
59
+
60
+ ## When to use it
61
+
62
+ - A Slack attachment came with `could not be downloaded` and a size over the
63
+ limit (1 GB by default), or the disk is too full to take it.
64
+ - The file is not on Slack at all (a phone video, a folder the owner zipped).
65
+ - **Never** answer a large file with "please compress it" or "put it in Drive".
66
+
67
+ Needs the machine to be signed in to Crewly Cloud. Files over about 100 MB
68
+ need the owner's paid plan (up to 2 GB); on the free plan the page tells the
69
+ owner so. The file is kept on a temporary server until you receive it
70
+ (`receive-file` deletes it) or for 7 days. Do not use it for secrets.
71
+
72
+ Options: `--purpose` (required, up to 300 characters, shown to the owner on the
73
+ upload page; say what the file is for), `--agent-name` (the name the owner sees,
74
+ default your session name), `--max-files` (1 to 10, default 10).
@@ -0,0 +1,89 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # request-file — ask the owner for a (large) file through a Crewly Cloud upload
4
+ # link (crewly-services apps/SPEC.md section 19).
5
+ #
6
+ # The backend (POST /api/apps/upload-requests) asks Cloud for a request; the
7
+ # owner opens the link on their phone and uploads in chunks; the machine's
8
+ # upload watcher tells the agent when the file is ready (receive-file).
9
+ #
10
+ # Usage:
11
+ # bash execute.sh --purpose <text> [--agent-name <name>] [--max-files <n>]
12
+ # bash execute.sh --status <requestId>
13
+ # bash execute.sh --cancel <requestId>
14
+ # =============================================================================
15
+ set -euo pipefail
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
18
+
19
+ print_usage() {
20
+ cat <<'EOF_USAGE'
21
+ Usage:
22
+ bash execute.sh --purpose <text> [--agent-name <name>] [--max-files <n>]
23
+ bash execute.sh --status <requestId>
24
+ bash execute.sh --cancel <requestId>
25
+
26
+ Options:
27
+ --purpose What you need the file for, in your words (required, up to 300 characters; the owner sees it)
28
+ --agent-name The name the owner sees (default: your session name)
29
+ --max-files How many files the link accepts, 1 to 10 (default 10)
30
+ --status Show a request: state, limits, files uploaded so far
31
+ --cancel Withdraw a link you no longer need
32
+ --help | -h Show this help
33
+ EOF_USAGE
34
+ }
35
+
36
+ fail_from() {
37
+ printf '%s' "$1" | jq -c '{success: false, status: (.status // 0), reason: (.details.error // .error // "unknown"), message: (.details.message // .details // "")}' 2>/dev/null \
38
+ || jq -cn --arg r "$1" '{success: false, reason: $r}'
39
+ exit 1
40
+ }
41
+
42
+ call() {
43
+ local out
44
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s\n' "$out" | tail -n 1)"; }
45
+ printf '%s\n' "$out" | tail -n 1
46
+ }
47
+
48
+ PURPOSE=""; AGENT_NAME=""; MAX_FILES=""; STATUS=""; CANCEL=""
49
+ while [[ $# -gt 0 ]]; do
50
+ case "$1" in
51
+ --purpose) [ $# -ge 2 ] || error_exit "--purpose requires a value"; PURPOSE="$2"; shift 2 ;;
52
+ --agent-name) [ $# -ge 2 ] || error_exit "--agent-name requires a value"; AGENT_NAME="$2"; shift 2 ;;
53
+ --max-files) [ $# -ge 2 ] || error_exit "--max-files requires a value"; MAX_FILES="$2"; shift 2 ;;
54
+ --status) [ $# -ge 2 ] || error_exit "--status requires a request id"; STATUS="$2"; shift 2 ;;
55
+ --cancel) [ $# -ge 2 ] || error_exit "--cancel requires a request id"; CANCEL="$2"; shift 2 ;;
56
+ --help|-h) print_usage; exit 0 ;;
57
+ *) error_exit "Unknown option: $1" ;;
58
+ esac
59
+ done
60
+
61
+ ID_RE='^ur_[A-Za-z0-9_-]{22}$'
62
+
63
+ if [ -n "$STATUS" ]; then
64
+ [[ "$STATUS" =~ $ID_RE ]] || error_exit "--status needs a request id like ur_…"
65
+ RESULT=$(call GET "/apps/upload-requests/${STATUS}") || { printf '%s\n' "$RESULT"; exit 1; }
66
+ printf '%s' "$RESULT" | jq -c '{success: true} + (.data | {requestId, status, purpose, expiresAt, files})'
67
+ exit 0
68
+ fi
69
+
70
+ if [ -n "$CANCEL" ]; then
71
+ [[ "$CANCEL" =~ $ID_RE ]] || error_exit "--cancel needs a request id like ur_…"
72
+ RESULT=$(call DELETE "/apps/upload-requests/${CANCEL}") || { printf '%s\n' "$RESULT"; exit 1; }
73
+ jq -cn --arg id "$CANCEL" '{success: true, requestId: $id, cancelled: true}'
74
+ exit 0
75
+ fi
76
+
77
+ [ -n "$PURPOSE" ] || error_exit "--purpose is required: say what you need the file for"
78
+ if [ -n "$MAX_FILES" ]; then
79
+ [[ "$MAX_FILES" =~ ^[0-9]+$ ]] && [ "$MAX_FILES" -ge 1 ] && [ "$MAX_FILES" -le 10 ] || error_exit "--max-files must be 1 to 10"
80
+ fi
81
+
82
+ BODY=$(jq -cn --arg p "$PURPOSE" --arg n "$AGENT_NAME" --arg m "$MAX_FILES" \
83
+ '{purpose: $p} + (if $n != "" then {agentName: $n} else {} end) + (if $m != "" then {maxFiles: ($m | tonumber)} else {} end)')
84
+ CREATED=$(call POST "/apps/upload-requests" "$BODY") || { printf '%s\n' "$CREATED"; exit 1; }
85
+ URL=$(printf '%s' "$CREATED" | jq -r '.data.url // empty')
86
+ [ -n "$URL" ] || error_exit "Cloud answered without an upload link"
87
+
88
+ printf '%s' "$CREATED" | jq -c '{success: true} + (.data | {requestId, url, expiresAt, limits, upgradeHint})
89
+ + {instruction: "Post the link to the owner in your own words (what you need and why; open it on the phone, pick the file from the camera roll). You will get a message with the receive-file command when the upload is done. Do not ask the owner to compress the file or use Drive."}'
@@ -0,0 +1,77 @@
1
+ #!/bin/bash
2
+ # Tests for request-file — run with: bash execute.test.sh
3
+ # A Python stub plays the Crewly backend.
4
+ set -u
5
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ EXEC="$HERE/execute.sh"
7
+ PASS=0; FAIL=0
8
+ check() { if [ "$2" = "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1"; echo " want: $3"; echo " got: $2"; fi; }
9
+
10
+ PORT=18862
11
+ STUB_LOG="$(mktemp)"; export STUB_LOG STUB_PORT="$PORT"
12
+ python3 -u <<'PY' >/dev/null 2>&1 &
13
+ from http.server import BaseHTTPRequestHandler, HTTPServer
14
+ import json, os
15
+ LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
16
+ ID = 'ur_AbCdEfGhIjKlMnOpQrStUv'
17
+ def note(**kw):
18
+ with open(LOG, 'a') as f: f.write(json.dumps(kw) + '\n')
19
+ class H(BaseHTTPRequestHandler):
20
+ def send(self, code, obj):
21
+ self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers(); self.wfile.write(json.dumps(obj).encode())
22
+ def body(self):
23
+ n = int(self.headers.get('Content-Length') or 0); return self.rfile.read(n)
24
+ def do_POST(self):
25
+ b = self.body()
26
+ if self.path == '/api/apps/upload-requests':
27
+ req = json.loads(b); note(op='create', req=req, session=self.headers.get('X-Agent-Session'))
28
+ if req['purpose'] == 'refuse':
29
+ return self.send(402, {'success': False, 'error': 'quota_exceeded', 'message': 'Too many open links.'})
30
+ return self.send(201, {'success': True, 'data': {'requestId': ID, 'url': 'https://crewlyai.com/portal/upload/' + ID, 'expiresAt': '2026-10-13T00:00:00.000Z', 'limits': {'maxFileBytes': 2147483648, 'maxFiles': 10}}})
31
+ self.send(404, {'success': False})
32
+ def do_GET(self):
33
+ note(op='status', path=self.path)
34
+ self.send(200, {'success': True, 'data': {'requestId': ID, 'status': 'open', 'purpose': 'p', 'expiresAt': 'x', 'files': [], 'limits': {}}})
35
+ def do_DELETE(self):
36
+ note(op='cancel', path=self.path); self.send(200, {'success': True, 'data': {'cancelled': True}})
37
+ def log_message(self, *a, **k): pass
38
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
39
+ PY
40
+ STUB_PID=$!
41
+ disown "$STUB_PID" 2>/dev/null || true
42
+ WORK="$(mktemp -d)"
43
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -rf "$STUB_LOG" "$WORK"' EXIT
44
+ for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
45
+ mkdir -p "$WORK/home/.crewly"
46
+ ENVS=(CREWLY_API_URL="http://127.0.0.1:${PORT}" CREWLY_SESSION_NAME=pia HOME="$WORK/home" CREWLY_HOME="$WORK/home/.crewly")
47
+ run() { env "${ENVS[@]}" bash "$EXEC" "$@" 2>/dev/null; }
48
+ run_err() { env "${ENVS[@]}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
49
+
50
+ : > "$STUB_LOG"
51
+ OUT=$(run --purpose "the raw footage" --agent-name Pia --max-files 2)
52
+ check "result" "$(printf '%s' "$OUT" | jq -c '{success, requestId, url}')" '{"success":true,"requestId":"ur_AbCdEfGhIjKlMnOpQrStUv","url":"https://crewlyai.com/portal/upload/ur_AbCdEfGhIjKlMnOpQrStUv"}'
53
+ check "instruction tells the agent not to ask for compression" "$(printf '%s' "$OUT" | jq -r .instruction | grep -c 'compress')" "1"
54
+ check "create request" "$(sed -n 1p "$STUB_LOG" | jq -c '{req, session}')" '{"req":{"purpose":"the raw footage","agentName":"Pia","maxFiles":2},"session":"pia"}'
55
+
56
+ : > "$STUB_LOG"
57
+ run --purpose "plain" >/dev/null
58
+ check "optional fields are left out" "$(sed -n 1p "$STUB_LOG" | jq -c .req)" '{"purpose":"plain"}'
59
+
60
+ OUT=$(run --purpose refuse; true)
61
+ check "cloud refusal passed through" "$(printf '%s' "$OUT" | jq -c '{success, reason}')" '{"success":false,"reason":"quota_exceeded"}'
62
+
63
+ : > "$STUB_LOG"
64
+ OUT=$(run --status ur_AbCdEfGhIjKlMnOpQrStUv)
65
+ check "status" "$(printf '%s' "$OUT" | jq -r .status)" "open"
66
+ check "status path" "$(jq -r .path "$STUB_LOG")" "/api/apps/upload-requests/ur_AbCdEfGhIjKlMnOpQrStUv"
67
+
68
+ : > "$STUB_LOG"
69
+ run --cancel ur_AbCdEfGhIjKlMnOpQrStUv >/dev/null
70
+ check "cancel path" "$(jq -r .path "$STUB_LOG")" "/api/apps/upload-requests/ur_AbCdEfGhIjKlMnOpQrStUv"
71
+
72
+ check "purpose required" "$(run_err | grep -c -- '--purpose is required')" "1"
73
+ check "bad max-files refused" "$(run_err --purpose x --max-files 99 | grep -c -- '--max-files must be 1 to 10')" "1"
74
+ check "bad id refused" "$(run_err --cancel nope | grep -c 'request id like ur_')" "1"
75
+
76
+ echo "request-file: $PASS passed, $FAIL failed"
77
+ [ "$FAIL" -eq 0 ]
@@ -49,5 +49,5 @@ the full file at `path`.
49
49
  | `not_visible` | No bot of your account is in the file's channel. Ask the owner to invite you (or the Crewly app) there. |
50
50
  | `foreign_workspace` | The link is from a Slack workspace your account has not connected. |
51
51
  | `file_deleted` | The file was deleted. |
52
- | `too_large` | Over 25 MB. |
52
+ | `too_large` | Over 25 MB for this command. A file the owner attached to a message is downloaded for you up to 1 GB; if it is still too big, or not on Slack, get it with the `request-file` skill (an upload link for the owner's phone). **Do not ask the owner to compress it or use Drive.** |
53
53
  | `no_file_in_message` | The message link has no file attached. |
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: Usage Categories
3
+ description: Save, list or remove the owner's own categories for usage reports (for example "self-media" and "book"). Categories are optional and only change how reports group tickets; they never alter ticket facts.
4
+ version: 1.0.0
5
+ category: monitoring
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - orchestrator
9
+ triggers:
10
+ - usage categories
11
+ - track time by category
12
+ - split usage into
13
+ - count self-media separately
14
+ tags:
15
+ - usage
16
+ - categories
17
+ - report
18
+ execution:
19
+ type: script
20
+ script:
21
+ file: execute.sh
22
+ interpreter: bash
23
+ timeoutMs: 20000
24
+ ---
25
+
26
+ # Usage Categories
27
+
28
+ The owner decides how usage should be grouped ("how much on self-media vs the book?"). Turn what they say into a category and save it. Nothing is required: with no categories, reports group by team.
29
+
30
+ ## Usage
31
+
32
+ ```bash
33
+ bash execute.sh list
34
+
35
+ # The description doubles as the keyword list when it is comma separated
36
+ bash execute.sh set --name self-media --description "Xiaohongshu, Substack, LinkedIn, videos"
37
+
38
+ # Explicit hints: keywords match ticket titles and text; teams / projects match by name
39
+ bash execute.sh set --name book --keywords "chapter,manuscript,Bible study book" --teams "Writers"
40
+
41
+ bash execute.sh remove --name book
42
+ bash execute.sh clear
43
+ ```
44
+
45
+ ## How a ticket gets its category
46
+
47
+ At report time, in this order:
48
+
49
+ 1. Keywords (case-insensitive), then team and project hints. The best score wins.
50
+ 2. If nothing matched and a small model is available, one batched question for the unmatched tickets.
51
+ 3. Otherwise `other`.
52
+
53
+ The result is cached per ticket and category set, and changes the moment a definition changes. Tickets themselves are never modified, so redefining categories re-slices all past usage.
54
+
55
+ ## Etiquette
56
+
57
+ - Use the owner's own words for names. Do not invent categories they did not ask for.
58
+ - After saving, offer to run `usage-report --by category` so they see the split.
59
+ - `other` is reserved and cannot be defined.
@@ -0,0 +1,81 @@
1
+ #!/bin/bash
2
+ # The owner's report categories ("self-media", "book", ...).
3
+ #
4
+ # Usage:
5
+ # execute.sh list
6
+ # execute.sh set --name self-media --description "Xiaohongshu, Substack, LinkedIn, videos" \
7
+ # [--keywords a,b,c] [--teams "Marketing,Media"] [--projects "Site,Newsletter"]
8
+ # execute.sh remove --name self-media
9
+ # execute.sh clear
10
+ # execute.sh '{"command":"set","name":"book","keywords":["chapter","manuscript"]}'
11
+ set -euo pipefail
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ source "${SCRIPT_DIR}/../_common/lib.sh"
14
+
15
+ COMMAND=""
16
+ NAME=""
17
+ DESCRIPTION=""
18
+ KEYWORDS=""
19
+ TEAMS=""
20
+ PROJECTS=""
21
+ INPUT_JSON=""
22
+
23
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
24
+ INPUT_JSON="$1"
25
+ shift || true
26
+ elif [[ $# -gt 0 && ${1:0:2} != '--' ]]; then
27
+ COMMAND="$1"
28
+ shift || true
29
+ fi
30
+
31
+ while [[ $# -gt 0 ]]; do
32
+ case "$1" in
33
+ --name) NAME="$2"; shift 2 ;;
34
+ --description) DESCRIPTION="$2"; shift 2 ;;
35
+ --keywords) KEYWORDS="$2"; shift 2 ;;
36
+ --teams) TEAMS="$2"; shift 2 ;;
37
+ --projects) PROJECTS="$2"; shift 2 ;;
38
+ --help|-h)
39
+ echo "Usage: execute.sh list | set --name N [--description D] [--keywords a,b] [--teams a,b] [--projects a,b] | remove --name N | clear"
40
+ exit 0 ;;
41
+ *) shift ;;
42
+ esac
43
+ done
44
+
45
+ if [[ -n "$INPUT_JSON" ]]; then
46
+ [[ -z "$COMMAND" ]] && COMMAND=$(echo "$INPUT_JSON" | jq -r '.command // empty' 2>/dev/null || true)
47
+ [[ -z "$NAME" ]] && NAME=$(echo "$INPUT_JSON" | jq -r '.name // empty' 2>/dev/null || true)
48
+ [[ -z "$DESCRIPTION" ]] && DESCRIPTION=$(echo "$INPUT_JSON" | jq -r '.description // empty' 2>/dev/null || true)
49
+ [[ -z "$KEYWORDS" ]] && KEYWORDS=$(echo "$INPUT_JSON" | jq -r '.keywords // empty | if type == "array" then join(",") else . end' 2>/dev/null || true)
50
+ [[ -z "$TEAMS" ]] && TEAMS=$(echo "$INPUT_JSON" | jq -r '.teams // empty | if type == "array" then join(",") else . end' 2>/dev/null || true)
51
+ [[ -z "$PROJECTS" ]] && PROJECTS=$(echo "$INPUT_JSON" | jq -r '.projects // empty | if type == "array" then join(",") else . end' 2>/dev/null || true)
52
+ fi
53
+
54
+ require_param "command (list|set|remove|clear)" "$COMMAND"
55
+
56
+ case "$COMMAND" in
57
+ list)
58
+ api_call GET "/usage-ledger/categories"
59
+ ;;
60
+ set)
61
+ require_param "name" "$NAME"
62
+ BODY=$(jq -n --arg name "$NAME" --arg description "$DESCRIPTION" --arg keywords "$KEYWORDS" --arg teams "$TEAMS" --arg projects "$PROJECTS" '
63
+ def list($s): $s | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0));
64
+ { name: $name }
65
+ + (if $description != "" then { description: $description } else {} end)
66
+ + (if $keywords != "" then { keywords: list($keywords) } else {} end)
67
+ + (if $teams != "" then { teams: list($teams) } else {} end)
68
+ + (if $projects != "" then { projects: list($projects) } else {} end)')
69
+ api_call POST "/usage-ledger/categories" "$BODY"
70
+ ;;
71
+ remove)
72
+ require_param "name" "$NAME"
73
+ api_call DELETE "/usage-ledger/categories/$(jq -rn --arg n "$NAME" '$n|@uri')"
74
+ ;;
75
+ clear)
76
+ api_call PUT "/usage-ledger/categories" '{"categories":[]}'
77
+ ;;
78
+ *)
79
+ error_exit "Unknown command: $COMMAND (use list, set, remove or clear)"
80
+ ;;
81
+ esac
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # Tests for usage-categories — run with: bash execute.test.sh
3
+ set -u
4
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ PASS=0; FAIL=0
6
+ check() { local name="$1"; local got="$2"; local want="$3"; if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi; }
7
+
8
+ TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
9
+ mkdir -p "$TMP/skills/orchestrator/usage-categories" "$TMP/skills/orchestrator/_common"
10
+ cp "$HERE/execute.sh" "$TMP/skills/orchestrator/usage-categories/execute.sh"
11
+ REAL_LIB="$(cd "$HERE/../../_common" && pwd)/lib.sh"
12
+ cat > "$TMP/skills/orchestrator/_common/lib.sh" <<'STUB'
13
+ source "$REAL_LIB"
14
+ api_call() { printf '%s|%s|%s' "$1" "$2" "${3:-}" > "$CALL_LOG"; echo '{"success":true}'; }
15
+ STUB
16
+ export REAL_LIB CALL_LOG="$TMP/call"
17
+
18
+ run() { CODE=0; OUT=$(env -u CREWLY_SESSION_NAME bash "$TMP/skills/orchestrator/usage-categories/execute.sh" "$@" 2>"$TMP/err") || CODE=$?; }
19
+
20
+ run list
21
+ check "list" "$(cat "$CALL_LOG")" "GET|/usage-ledger/categories|"
22
+
23
+ run set --name self-media --description "Xiaohongshu, Substack, LinkedIn" --keywords "videos, shorts" --teams "Marketing"
24
+ LOG=$(cat "$CALL_LOG"); M=${LOG%%|*}; REST=${LOG#*|}; P=${REST%%|*}; B=${REST#*|}
25
+ check "set uses POST" "$M $P" "POST /usage-ledger/categories"
26
+ check "set body" "$(printf '%s' "$B" | jq -c '.')" '{"name":"self-media","description":"Xiaohongshu, Substack, LinkedIn","keywords":["videos","shorts"],"teams":["Marketing"]}'
27
+
28
+ run '{"command":"set","name":"book","keywords":["chapter","manuscript"]}'
29
+ LOG=$(cat "$CALL_LOG"); B=${LOG#*|*|}
30
+ check "JSON input set body" "$(printf '%s' "$B" | jq -c '.')" '{"name":"book","keywords":["chapter","manuscript"]}'
31
+
32
+ run remove --name "Bible study"
33
+ check "remove encodes the name" "$(cat "$CALL_LOG")" "DELETE|/usage-ledger/categories/Bible%20study|"
34
+
35
+ run clear
36
+ check "clear" "$(cat "$CALL_LOG")" 'PUT|/usage-ledger/categories|{"categories":[]}'
37
+
38
+ run set
39
+ check "set needs a name" "$CODE" "1"
40
+
41
+ run frobnicate
42
+ check "unknown command" "$CODE" "1"
43
+
44
+ echo "=== Results: $PASS passed, $FAIL failed ==="
45
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: Usage Report
3
+ description: How much owner time, agent time and tokens went into each category, team, project, agent or ticket (for example self-media vs the book). Computed from the immutable ticket ledger; categories are applied at report time.
4
+ version: 1.0.0
5
+ category: monitoring
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - orchestrator
9
+ triggers:
10
+ - usage report
11
+ - how much time did we spend
12
+ - how many tokens
13
+ - time and tokens per project
14
+ - where did the tokens go
15
+ tags:
16
+ - usage
17
+ - tokens
18
+ - time
19
+ - cost
20
+ - report
21
+ execution:
22
+ type: script
23
+ script:
24
+ file: execute.sh
25
+ interpreter: bash
26
+ timeoutMs: 60000
27
+ ---
28
+
29
+ # Usage Report
30
+
31
+ Answers "how much time and how many tokens went into X?". Every ticket's facts (agent busy time, tokens and estimated cost per model, owner active minutes) are recorded as they happen and never change. This skill adds them up for a window and groups them the way the owner asks.
32
+
33
+ ## Usage
34
+
35
+ ```bash
36
+ # Last 7 days by category (the owner's categories, else by team)
37
+ bash execute.sh --since 7d --by category
38
+
39
+ # Just the short text to read out or post to Slack
40
+ bash execute.sh --since 30d --by category --text
41
+
42
+ # Compare two of the owner's categories
43
+ bash execute.sh --since 14d --by category --categories "self-media,book"
44
+
45
+ # One-off categories (not saved): name:keyword,keyword;name:keyword
46
+ bash execute.sh --since 7d --categories "media:xiaohongshu,substack,linkedin;book:chapter,bible"
47
+
48
+ # Other groupings
49
+ bash execute.sh --since 7d --by team
50
+ bash execute.sh --since 7d --by project
51
+ bash execute.sh --since 7d --by agent
52
+ bash execute.sh --since 2d --by ticket --limit 10
53
+ ```
54
+
55
+ `--since` takes `24h`, `7d`, `2w` or a date. Figures are summed per UTC hour, so a window edge is accurate to the hour.
56
+
57
+ ## Categories
58
+
59
+ Categories are never required and never stored on tickets. If the owner has defined categories (see the `usage-categories` skill) the report uses them. If not, tickets are grouped by team (then project). A ticket that matches no category is `other`; usage that happened outside any ticket is `(no ticket)`.
60
+
61
+ When the owner asks for a new grouping ("how much on self-media vs the book?"), either pass `--categories` for one report or save it with `usage-categories set`.
62
+
63
+ ## Output
64
+
65
+ Each group has: owner minutes, agent busy minutes, tokens (cost-weighted: cache reads count 0.1x), raw tokens, estimated cost in USD, ticket count and its top 5 tickets. `summary` is a short text suitable for speech or Slack. `notes` lists caveats (for example tickets rebuilt from history are marked inferred).
66
+
67
+ ## Reading the numbers to the owner
68
+
69
+ - Lead with the summary line, then the two or three biggest groups.
70
+ - Owner minutes are an estimate from message times (messages on one ticket within 10 minutes form one span).
71
+ - Say "about" for cost; it is an API-price estimate, not a bill.
@@ -0,0 +1,84 @@
1
+ #!/bin/bash
2
+ # Usage report: where the owner's time and the agents' tokens went.
3
+ #
4
+ # Usage:
5
+ # execute.sh [--since 7d] [--until 2026-10-11] [--by category|team|project|agent|ticket]
6
+ # [--categories "book,self-media" | "media:linkedin,substack;book:chapter"]
7
+ # [--limit 10] [--text]
8
+ # execute.sh '{"since":"7d","by":"category"}'
9
+ #
10
+ # --text prints only the short summary (good to read out or post to Slack).
11
+ # Read-only: it changes nothing.
12
+ set -euo pipefail
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ source "${SCRIPT_DIR}/../_common/lib.sh"
15
+
16
+ SINCE=""
17
+ UNTIL=""
18
+ BY=""
19
+ CATEGORIES=""
20
+ LIMIT=""
21
+ TEXT="false"
22
+ INPUT_JSON=""
23
+
24
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
25
+ INPUT_JSON="$1"
26
+ shift || true
27
+ fi
28
+
29
+ while [[ $# -gt 0 ]]; do
30
+ case "$1" in
31
+ --since) SINCE="$2"; shift 2 ;;
32
+ --until) UNTIL="$2"; shift 2 ;;
33
+ --by) BY="$2"; shift 2 ;;
34
+ --categories) CATEGORIES="$2"; shift 2 ;;
35
+ --limit) LIMIT="$2"; shift 2 ;;
36
+ --text) TEXT="true"; shift ;;
37
+ --help|-h)
38
+ echo "Usage: execute.sh [--since 7d] [--until DATE] [--by category|team|project|agent|ticket] [--categories NAMES_OR_DEFS] [--limit N] [--text]"
39
+ exit 0 ;;
40
+ *) shift ;;
41
+ esac
42
+ done
43
+
44
+ if [[ -n "$INPUT_JSON" ]]; then
45
+ [[ -z "$SINCE" ]] && SINCE=$(echo "$INPUT_JSON" | jq -r '.since // empty' 2>/dev/null || true)
46
+ [[ -z "$UNTIL" ]] && UNTIL=$(echo "$INPUT_JSON" | jq -r '.until // empty' 2>/dev/null || true)
47
+ [[ -z "$BY" ]] && BY=$(echo "$INPUT_JSON" | jq -r '.by // empty' 2>/dev/null || true)
48
+ [[ -z "$CATEGORIES" ]] && CATEGORIES=$(echo "$INPUT_JSON" | jq -r '.categories // empty | if type == "array" then tojson else . end' 2>/dev/null || true)
49
+ [[ -z "$LIMIT" ]] && LIMIT=$(echo "$INPUT_JSON" | jq -r '.limit // empty' 2>/dev/null || true)
50
+ if [[ "$TEXT" != "true" ]]; then TEXT=$(echo "$INPUT_JSON" | jq -r '.text // false' 2>/dev/null || echo false); fi
51
+ fi
52
+
53
+ QUERY=$(jq -rn \
54
+ --arg since "${SINCE:-7d}" --arg until "$UNTIL" --arg by "${BY:-category}" \
55
+ --arg categories "$CATEGORIES" --arg limit "$LIMIT" \
56
+ '[ "since=" + ($since|@uri), "by=" + ($by|@uri),
57
+ (if $until != "" then "until=" + ($until|@uri) else empty end),
58
+ (if $categories != "" then "categories=" + ($categories|@uri) else empty end),
59
+ (if $limit != "" then "limit=" + ($limit|@uri) else empty end) ] | join("&")')
60
+
61
+ if ! RESP=$(api_call_full GET "/usage-ledger/report?${QUERY}"); then
62
+ error_exit "Could not fetch the usage report"
63
+ fi
64
+
65
+ if [[ "$TEXT" == "true" ]]; then
66
+ printf '%s' "$RESP" | jq -r '.data.summary // .error // "No report"'
67
+ exit 0
68
+ fi
69
+
70
+ # Compact: the full body has every token kind for every group.
71
+ printf '%s' "$RESP" | jq '
72
+ if .success then
73
+ { summary: .data.summary,
74
+ window: .data.window,
75
+ by: .data.by,
76
+ categories: .data.categories,
77
+ totals: { ownerMinutes: .data.totals.ownerMinutes, agentBusyMinutes: .data.totals.agentBusyMinutes,
78
+ tokens: .data.totals.tokens.budget, rawTokens: .data.totals.tokens.total,
79
+ costUsd: .data.totals.costUsd, tickets: .data.totals.ticketCount },
80
+ groups: [ .data.groups[] | { key, ownerMinutes, agentBusyMinutes, tokens: .tokens.budget, rawTokens: .tokens.total,
81
+ costUsd, tickets: .ticketCount,
82
+ topTickets: [ .topTickets[] | { ticketId, title, ownerMinutes, agentBusyMinutes, tokens, costUsd } ] } ],
83
+ notes: .data.notes }
84
+ else . end'