@blastin-dev/clocktopus-cli 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +108 -5
  2. package/dist/src/commands/agent/disable.d.ts +14 -0
  3. package/dist/src/commands/agent/disable.d.ts.map +1 -0
  4. package/dist/src/commands/agent/disable.js +72 -0
  5. package/dist/src/commands/agent/doctor.d.ts +2 -0
  6. package/dist/src/commands/agent/doctor.d.ts.map +1 -0
  7. package/dist/src/commands/agent/doctor.js +235 -0
  8. package/dist/src/commands/agent/hook.d.ts +2 -0
  9. package/dist/src/commands/agent/hook.d.ts.map +1 -0
  10. package/dist/src/commands/agent/hook.js +231 -0
  11. package/dist/src/commands/agent/setup.d.ts +21 -0
  12. package/dist/src/commands/agent/setup.d.ts.map +1 -0
  13. package/dist/src/commands/agent/setup.js +194 -0
  14. package/dist/src/commands/agent/status.d.ts +2 -0
  15. package/dist/src/commands/agent/status.d.ts.map +1 -0
  16. package/dist/src/commands/agent/status.js +160 -0
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/src/index.js +37 -2
  19. package/dist/src/lib/agent-config.d.ts +41 -0
  20. package/dist/src/lib/agent-config.d.ts.map +1 -0
  21. package/dist/src/lib/agent-config.js +143 -0
  22. package/dist/src/lib/agent-hook-state.d.ts +36 -0
  23. package/dist/src/lib/agent-hook-state.d.ts.map +1 -0
  24. package/dist/src/lib/agent-hook-state.js +136 -0
  25. package/dist/src/lib/agent-receiver.d.ts +26 -0
  26. package/dist/src/lib/agent-receiver.d.ts.map +1 -0
  27. package/dist/src/lib/agent-receiver.js +44 -0
  28. package/dist/src/lib/claude-settings.d.ts +82 -0
  29. package/dist/src/lib/claude-settings.d.ts.map +1 -0
  30. package/dist/src/lib/claude-settings.js +271 -0
  31. package/dist/src/lib/claude-settings.test.d.ts +2 -0
  32. package/dist/src/lib/claude-settings.test.d.ts.map +1 -0
  33. package/dist/src/lib/claude-settings.test.js +193 -0
  34. package/dist/src/lib/config.d.ts +23 -0
  35. package/dist/src/lib/config.d.ts.map +1 -1
  36. package/dist/src/lib/config.js +14 -0
  37. package/dist/src/lib/format.d.ts +6 -0
  38. package/dist/src/lib/format.d.ts.map +1 -0
  39. package/dist/src/lib/format.js +19 -0
  40. package/dist/src/lib/git.d.ts +3 -0
  41. package/dist/src/lib/git.d.ts.map +1 -0
  42. package/dist/src/lib/git.js +30 -0
  43. package/dist/src/lib/repo-guidance.d.ts +40 -0
  44. package/dist/src/lib/repo-guidance.d.ts.map +1 -0
  45. package/dist/src/lib/repo-guidance.js +123 -0
  46. package/dist/src/lib/validators.d.ts +69 -0
  47. package/dist/src/lib/validators.d.ts.map +1 -1
  48. package/dist/src/lib/validators.js +69 -0
  49. package/package.json +6 -4
@@ -0,0 +1,123 @@
1
+ import { ApiError, get } from "./api.js";
2
+ import { getApiUrl } from "./config.js";
3
+ import { formatAgo } from "./format.js";
4
+ import { getRepositoryRemote } from "./git.js";
5
+ import { RepoStatusSchema } from "./validators.js";
6
+ /**
7
+ * Fetches readiness for the repository the CLI is standing in.
8
+ *
9
+ * Returns null when there is nothing to ask about — outside a git tree, or
10
+ * when the server is unreachable. A failure here must never fail `setup`:
11
+ * the local configuration it just wrote is valid regardless.
12
+ */
13
+ export async function fetchRepoStatus(cwd = process.cwd()) {
14
+ const remote = getRepositoryRemote(cwd);
15
+ if (!remote)
16
+ return null;
17
+ try {
18
+ const data = await get(`/api/agent/repo-status?repositoryUrl=${encodeURIComponent(remote)}`);
19
+ return RepoStatusSchema.parse(data);
20
+ }
21
+ catch (error) {
22
+ if (error instanceof ApiError)
23
+ return null;
24
+ return null;
25
+ }
26
+ }
27
+ export function attributionCheck(status) {
28
+ const { attribution, repository } = status;
29
+ const where = repository?.displayUrl ?? "this repository";
30
+ switch (attribution.reason) {
31
+ case "attributed":
32
+ return {
33
+ label: "Project",
34
+ ok: true,
35
+ detail: attribution.projectNames[0] ?? "attached",
36
+ };
37
+ case "no_matching_project":
38
+ return {
39
+ label: "Project",
40
+ ok: false,
41
+ detail: `${where} is not attached to any project`,
42
+ fix: "Attach it under Projects → (your project) → Repositories.\n" +
43
+ " Until then this repo's agent spend is recorded, but lands in\n" +
44
+ " the unattributed row instead of against a client.",
45
+ };
46
+ case "ambiguous_project":
47
+ return {
48
+ label: "Project",
49
+ ok: false,
50
+ // Refusing to guess is the designed behaviour, not a bug — billing
51
+ // one client for another's work is worse than billing nobody.
52
+ detail: `${where} is attached to ${attribution.projectNames.length} projects (${attribution.projectNames.join(", ")})`,
53
+ fix: "Clocktopus will not guess which client to bill, so these sessions\n" +
54
+ " stay unattributed. Remove the repository from all but one project.",
55
+ };
56
+ case "unsupported_remote":
57
+ return {
58
+ label: "Project",
59
+ ok: "warn",
60
+ detail: "remote is not GitHub or Bitbucket",
61
+ fix: "Spend from this repository is recorded but cannot be attributed —\n" +
62
+ " only GitHub and Bitbucket remotes resolve to a project.",
63
+ };
64
+ case "no_repository":
65
+ return {
66
+ label: "Project",
67
+ ok: "warn",
68
+ detail: "not a git repository",
69
+ fix: "Run 'clocktopus agent setup' inside a project checkout to check attribution.",
70
+ };
71
+ }
72
+ }
73
+ export function commitLaneCheck(status) {
74
+ const dashboard = getApiUrl().replace(/\/$/, "");
75
+ switch (status.commitLane.status) {
76
+ case "delivering":
77
+ return {
78
+ label: "Commit lane",
79
+ ok: true,
80
+ detail: `last commit received ${formatAgo(status.commitLane.lastCommitAt)}`,
81
+ };
82
+ case "no_secret":
83
+ return {
84
+ label: "Commit lane",
85
+ ok: false,
86
+ detail: "no webhook secret — no commits are reaching Clocktopus",
87
+ fix: `Create one at ${dashboard}/settings/webhooks, then add a push\n` +
88
+ ` webhook to this repository. Guide:\n` +
89
+ ` ${dashboard}/docs/guides/setup-github-webhook`,
90
+ };
91
+ case "no_delivery":
92
+ return {
93
+ label: "Commit lane",
94
+ ok: false,
95
+ detail: "no commit has ever arrived from this repository",
96
+ fix: "Agent spend will be recorded here, but with no human time beside it\n" +
97
+ " the true-cost comparison has only one side. Add a push webhook:\n" +
98
+ ` URL ${status.webhookUrl}\n` +
99
+ " Events push\n" +
100
+ ` Secret ${dashboard}/settings/webhooks\n` +
101
+ " The URL is per person, not per repository — on a shared repo\n" +
102
+ " each teammate adds their own.",
103
+ };
104
+ }
105
+ }
106
+ export function repoChecks(status) {
107
+ return [attributionCheck(status), commitLaneCheck(status)];
108
+ }
109
+ /** The block `agent setup` prints after writing the local configuration. */
110
+ export function renderRepoBlock(status) {
111
+ const lines = [];
112
+ const heading = status.repository?.displayUrl ?? "this directory";
113
+ lines.push("");
114
+ lines.push(`Repository ${heading}`);
115
+ lines.push("");
116
+ for (const check of repoChecks(status)) {
117
+ const symbol = check.ok === true ? "✓" : check.ok === "warn" ? "⚠" : "✗";
118
+ lines.push(` ${symbol} ${check.label.padEnd(12)}${check.detail}`);
119
+ if (check.ok !== true && check.fix)
120
+ lines.push(` ${check.fix}`);
121
+ }
122
+ return lines;
123
+ }
@@ -36,4 +36,73 @@ export declare const TokenErrorResponseSchema: z.ZodObject<{
36
36
  }, z.core.$strip>;
37
37
  export type TokenResponse = z.infer<typeof TokenResponseSchema>;
38
38
  export type TokenErrorResponse = z.infer<typeof TokenErrorResponseSchema>;
39
+ export declare const IngestTokenSchema: z.ZodObject<{
40
+ tokenId: z.ZodString;
41
+ token: z.ZodString;
42
+ tokenPrefix: z.ZodString;
43
+ endpoint: z.ZodString;
44
+ }, z.core.$strip>;
45
+ export declare const AgentEndpointSchema: z.ZodObject<{
46
+ endpoint: z.ZodString;
47
+ }, z.core.$strip>;
48
+ export declare const AgentStatusSchema: z.ZodObject<{
49
+ endpoint: z.ZodString;
50
+ window: z.ZodObject<{
51
+ since: z.ZodString;
52
+ sessions: z.ZodNumber;
53
+ unattributedSessions: z.ZodNumber;
54
+ costMicroUsd: z.ZodNumber;
55
+ }, z.core.$strip>;
56
+ hookLane: z.ZodObject<{
57
+ lastSessionStartedAt: z.ZodNullable<z.ZodString>;
58
+ repositoryUrl: z.ZodNullable<z.ZodString>;
59
+ gitBranch: z.ZodNullable<z.ZodString>;
60
+ cwd: z.ZodNullable<z.ZodString>;
61
+ attributed: z.ZodNullable<z.ZodBoolean>;
62
+ }, z.core.$strip>;
63
+ metricsLane: z.ZodObject<{
64
+ lastExportReceivedAt: z.ZodNullable<z.ZodString>;
65
+ lastModel: z.ZodNullable<z.ZodString>;
66
+ }, z.core.$strip>;
67
+ tokens: z.ZodArray<z.ZodObject<{
68
+ id: z.ZodString;
69
+ name: z.ZodNullable<z.ZodString>;
70
+ tokenPrefix: z.ZodString;
71
+ lastUsedAt: z.ZodNullable<z.ZodString>;
72
+ usageCount: z.ZodNumber;
73
+ createdAt: z.ZodString;
74
+ }, z.core.$strip>>;
75
+ }, z.core.$strip>;
76
+ export declare const RepoStatusSchema: z.ZodObject<{
77
+ repository: z.ZodNullable<z.ZodObject<{
78
+ provider: z.ZodString;
79
+ ownerRepo: z.ZodString;
80
+ displayUrl: z.ZodString;
81
+ }, z.core.$strip>>;
82
+ attribution: z.ZodObject<{
83
+ reason: z.ZodEnum<{
84
+ attributed: "attributed";
85
+ no_repository: "no_repository";
86
+ unsupported_remote: "unsupported_remote";
87
+ no_matching_project: "no_matching_project";
88
+ ambiguous_project: "ambiguous_project";
89
+ }>;
90
+ projectId: z.ZodNullable<z.ZodString>;
91
+ projectNames: z.ZodArray<z.ZodString>;
92
+ }, z.core.$strip>;
93
+ commitLane: z.ZodObject<{
94
+ status: z.ZodEnum<{
95
+ delivering: "delivering";
96
+ no_secret: "no_secret";
97
+ no_delivery: "no_delivery";
98
+ }>;
99
+ lastCommitAt: z.ZodNullable<z.ZodString>;
100
+ hasWebhookSecret: z.ZodBoolean;
101
+ }, z.core.$strip>;
102
+ ready: z.ZodBoolean;
103
+ webhookUrl: z.ZodString;
104
+ }, z.core.$strip>;
105
+ export type IngestTokenResponse = z.infer<typeof IngestTokenSchema>;
106
+ export type AgentStatusResponse = z.infer<typeof AgentStatusSchema>;
107
+ export type RepoStatusResponse = z.infer<typeof RepoStatusSchema>;
39
108
  //# sourceMappingURL=validators.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../src/lib/validators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,UAAU;;;;;;;;;iBASrB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,eAAO,MAAM,wBAAwB;;;;;;;iBAOnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,eAAO,MAAM,mBAAmB;;;;iBAI9B,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;iBASnC,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../src/lib/validators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,UAAU;;;;;;;;;iBASrB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,eAAO,MAAM,wBAAwB;;;;;;;iBAOnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,eAAO,MAAM,mBAAmB;;;;iBAI9B,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;iBASnC,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAO1E,eAAO,MAAM,iBAAiB;;;;;iBAM5B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;iBAE9B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6B5B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0B3B,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACpE,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACpE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
@@ -32,3 +32,72 @@ export const TokenErrorResponseSchema = z.object({
32
32
  ]),
33
33
  error_description: z.string().optional(),
34
34
  });
35
+ // Agent telemetry (`clocktopus agent …`). The endpoint is always supplied by
36
+ // the server rather than known to the CLI: the receiver lives on its own
37
+ // Router whose URL differs per stage, so a baked-in default would be wrong
38
+ // everywhere but production.
39
+ export const IngestTokenSchema = z.object({
40
+ tokenId: z.string(),
41
+ /** Plaintext, returned exactly once — the server stores only a hash. */
42
+ token: z.string(),
43
+ tokenPrefix: z.string(),
44
+ endpoint: z.string(),
45
+ });
46
+ export const AgentEndpointSchema = z.object({
47
+ endpoint: z.string(),
48
+ });
49
+ export const AgentStatusSchema = z.object({
50
+ endpoint: z.string(),
51
+ window: z.object({
52
+ since: z.string(),
53
+ sessions: z.number(),
54
+ unattributedSessions: z.number(),
55
+ costMicroUsd: z.number(),
56
+ }),
57
+ hookLane: z.object({
58
+ lastSessionStartedAt: z.string().nullable(),
59
+ repositoryUrl: z.string().nullable(),
60
+ gitBranch: z.string().nullable(),
61
+ cwd: z.string().nullable(),
62
+ attributed: z.boolean().nullable(),
63
+ }),
64
+ metricsLane: z.object({
65
+ lastExportReceivedAt: z.string().nullable(),
66
+ lastModel: z.string().nullable(),
67
+ }),
68
+ tokens: z.array(z.object({
69
+ id: z.string(),
70
+ name: z.string().nullable(),
71
+ tokenPrefix: z.string(),
72
+ lastUsedAt: z.string().nullable(),
73
+ usageCount: z.number(),
74
+ createdAt: z.string(),
75
+ })),
76
+ });
77
+ export const RepoStatusSchema = z.object({
78
+ repository: z
79
+ .object({
80
+ provider: z.string(),
81
+ ownerRepo: z.string(),
82
+ displayUrl: z.string(),
83
+ })
84
+ .nullable(),
85
+ attribution: z.object({
86
+ reason: z.enum([
87
+ "attributed",
88
+ "no_repository",
89
+ "unsupported_remote",
90
+ "no_matching_project",
91
+ "ambiguous_project",
92
+ ]),
93
+ projectId: z.string().nullable(),
94
+ projectNames: z.array(z.string()),
95
+ }),
96
+ commitLane: z.object({
97
+ status: z.enum(["delivering", "no_secret", "no_delivery"]),
98
+ lastCommitAt: z.string().nullable(),
99
+ hasWebhookSecret: z.boolean(),
100
+ }),
101
+ ready: z.boolean(),
102
+ webhookUrl: z.string(),
103
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blastin-dev/clocktopus-cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "clocktopus": "./dist/bin/clocktopus.js"
@@ -12,13 +12,14 @@
12
12
  "commander": "^13.0.0",
13
13
  "conf": "^13.0.0",
14
14
  "date-fns": "4.1.0",
15
- "zod": "4.1.12"
15
+ "zod": "4.4.3"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/node": "22.15.3",
19
19
  "eslint": "9.37.0",
20
20
  "eslint-plugin-import-x": "^4.16.1",
21
21
  "typescript": "5.9.2",
22
+ "vitest": "^4.1.5",
22
23
  "@repo/eslint-config": "0.0.0",
23
24
  "@repo/prettier-config": "0.1.0",
24
25
  "@repo/typescript-config": "0.0.0"
@@ -28,8 +29,9 @@
28
29
  "build": "tsc",
29
30
  "dev": "tsc --watch",
30
31
  "check-types": "tsc --noEmit",
32
+ "test": "vitest run",
31
33
  "lint": "eslint",
32
- "format": "prettier --check . --ignore-path ../../.gitignore",
33
- "format:fix": "prettier --write . --ignore-path ../../.gitignore"
34
+ "format": "prettier --check . --ignore-path ../../.gitignore --ignore-path ../../.prettierignore",
35
+ "format:fix": "prettier --write . --ignore-path ../../.gitignore --ignore-path ../../.prettierignore"
34
36
  }
35
37
  }