@yhong91/vibetime 0.1.65 → 0.1.67

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 (2) hide show
  1. package/bin/vibetime.mjs +254 -83
  2. package/package.json +1 -1
package/bin/vibetime.mjs CHANGED
@@ -2047,7 +2047,7 @@ function claudeStyleFileMetrics(tool, input) {
2047
2047
  }
2048
2048
 
2049
2049
  // src/lib/constants.ts
2050
- var PACKAGE_VERSION = true ? "0.1.65" : "0.1.1";
2050
+ var PACKAGE_VERSION = true ? "0.1.67" : "0.1.1";
2051
2051
  var GENERATED_MARKER = "Generated by vibetime.";
2052
2052
  var DEFAULT_API_URL = "http://121.196.224.82:3001";
2053
2053
  var DEFAULT_BACKFILL_BATCH_SIZE = 50;
@@ -5832,12 +5832,46 @@ async function parseCopilotSessionFile(filePath, options) {
5832
5832
  let project;
5833
5833
  let model;
5834
5834
  let currentTurnId;
5835
+ let currentTurnStartedAt;
5836
+ let currentTurnLastEventAt;
5835
5837
  let sessionStartedAt;
5836
5838
  let reasoningEffort;
5839
+ let emittedShutdownUsage = false;
5840
+ let sawWorkSinceShutdown = false;
5841
+ const lastShutdownUsage = /* @__PURE__ */ new Map();
5837
5842
  const pendingTools = /* @__PURE__ */ new Map();
5838
5843
  const turnTokenAccum = /* @__PURE__ */ new Map();
5839
- const pendingTurnUsage = [];
5840
- let emittedShutdownUsage = false;
5844
+ const noteWork = (ts) => {
5845
+ if (currentTurnId) {
5846
+ currentTurnLastEventAt = ts;
5847
+ }
5848
+ sawWorkSinceShutdown = true;
5849
+ };
5850
+ const closeOpenTurn = () => {
5851
+ if (!currentTurnId || !currentTurnLastEventAt && !currentTurnStartedAt) {
5852
+ return;
5853
+ }
5854
+ const closedAt = currentTurnLastEventAt || currentTurnStartedAt;
5855
+ if (!closedAt) {
5856
+ return;
5857
+ }
5858
+ events.push(baseCopilotEvent({
5859
+ ts: closedAt,
5860
+ type: "turn.completed",
5861
+ sessionId,
5862
+ turnId: currentTurnId,
5863
+ cwd,
5864
+ project,
5865
+ model,
5866
+ confidence: "derived"
5867
+ }));
5868
+ };
5869
+ const releaseTurn = () => {
5870
+ closeOpenTurn();
5871
+ currentTurnId = void 0;
5872
+ currentTurnStartedAt = void 0;
5873
+ currentTurnLastEventAt = void 0;
5874
+ };
5841
5875
  for (const [index, line] of lines.entries()) {
5842
5876
  const lineNumber = index + 1;
5843
5877
  const raw = parseJsonLine(line);
@@ -5875,7 +5909,11 @@ async function parseCopilotSessionFile(filePath, options) {
5875
5909
  break;
5876
5910
  }
5877
5911
  case "user.message": {
5912
+ closeOpenTurn();
5878
5913
  currentTurnId = `turn_${createStableHash([sessionId, lineNumber]).slice(0, 24)}`;
5914
+ currentTurnStartedAt = ts;
5915
+ currentTurnLastEventAt = ts;
5916
+ sawWorkSinceShutdown = true;
5879
5917
  events.push(baseCopilotEvent({
5880
5918
  ts,
5881
5919
  type: "turn.started",
@@ -5913,8 +5951,8 @@ async function parseCopilotSessionFile(filePath, options) {
5913
5951
  if (msgModel) {
5914
5952
  model = msgModel;
5915
5953
  }
5916
- const msgTurnId = stringField(data, "turnId");
5917
- const turnId = msgTurnId === void 0 ? currentTurnId : `turn_${createStableHash([sessionId, msgTurnId]).slice(0, 24)}`;
5954
+ const turnId = currentTurnId;
5955
+ noteWork(ts);
5918
5956
  events.push(baseCopilotEvent({
5919
5957
  ts,
5920
5958
  type: "agent.operation",
@@ -5928,20 +5966,20 @@ async function parseCopilotSessionFile(filePath, options) {
5928
5966
  metrics: { modelCalls: 1 }
5929
5967
  }));
5930
5968
  const outputTokens = numberField(data, "outputTokens");
5931
- if (outputTokens && outputTokens > 0) {
5932
- const key = turnId || "unknown";
5933
- const accum = turnTokenAccum.get(key) || { tokensOutput: 0, modelCalls: 0 };
5934
- accum.tokensOutput = (accum.tokensOutput || 0) + outputTokens;
5935
- accum.modelCalls = (accum.modelCalls || 0) + 1;
5936
- turnTokenAccum.set(key, accum);
5969
+ if (turnId && outputTokens && outputTokens > 0) {
5970
+ const accum = turnTokenAccum.get(turnId) || { metrics: { tokensOutput: 0, modelCalls: 0 }, ts };
5971
+ accum.metrics.tokensOutput = (accum.metrics.tokensOutput || 0) + outputTokens;
5972
+ accum.metrics.modelCalls = (accum.metrics.modelCalls || 0) + 1;
5973
+ accum.ts = ts;
5974
+ turnTokenAccum.set(turnId, accum);
5937
5975
  }
5938
5976
  break;
5939
5977
  }
5940
5978
  case "tool.execution_start": {
5941
5979
  const toolCallId = stringField(data, "toolCallId");
5942
5980
  const toolName = stringField(data, "toolName") || "tool";
5943
- const toolTurnId = stringField(data, "turnId");
5944
- const turnId = toolTurnId === void 0 ? currentTurnId : `turn_${createStableHash([sessionId, toolTurnId]).slice(0, 24)}`;
5981
+ const turnId = currentTurnId;
5982
+ noteWork(ts);
5945
5983
  if (toolCallId) {
5946
5984
  pendingTools.set(toolCallId, { tool: toolName, startedAt: ts, turnId });
5947
5985
  }
@@ -5985,6 +6023,7 @@ async function parseCopilotSessionFile(filePath, options) {
5985
6023
  if (toolCallId) {
5986
6024
  pendingTools.delete(toolCallId);
5987
6025
  }
6026
+ noteWork(ts);
5988
6027
  const success = data.success !== false;
5989
6028
  const durationMs = pending ? Math.max(0, new Date(ts).getTime() - new Date(pending.startedAt).getTime()) : void 0;
5990
6029
  events.push(baseCopilotEvent({
@@ -6027,54 +6066,27 @@ async function parseCopilotSessionFile(filePath, options) {
6027
6066
  break;
6028
6067
  }
6029
6068
  case "assistant.turn_end": {
6030
- const endTurnId = stringField(data, "turnId");
6031
- const turnId = endTurnId === void 0 ? currentTurnId : `turn_${createStableHash([sessionId, endTurnId]).slice(0, 24)}`;
6032
- const accum = turnId ? turnTokenAccum.get(turnId) : void 0;
6033
- if (accum && accum.tokensOutput && accum.tokensOutput > 0) {
6034
- accum.reasoningEffort = reasoningEffort;
6035
- pendingTurnUsage.push(baseCopilotEvent({
6036
- ts,
6037
- type: "model.usage",
6038
- sessionId,
6039
- turnId,
6040
- cwd,
6041
- project,
6042
- model,
6043
- confidence: "partial",
6044
- metrics: accum
6045
- }));
6046
- turnTokenAccum.delete(turnId);
6047
- }
6048
- events.push(baseCopilotEvent({
6049
- ts,
6050
- type: "turn.completed",
6051
- sessionId,
6052
- turnId,
6053
- cwd,
6054
- project,
6055
- model,
6056
- confidence: "derived"
6057
- }));
6069
+ break;
6070
+ }
6071
+ case "session.resume": {
6072
+ releaseTurn();
6058
6073
  break;
6059
6074
  }
6060
6075
  case "session.shutdown": {
6076
+ releaseTurn();
6061
6077
  const modelMetrics2 = objectField(data, "modelMetrics");
6062
6078
  for (const [modelName, metricsRaw] of Object.entries(modelMetrics2)) {
6063
6079
  if (!isPlainObject(metricsRaw)) {
6064
6080
  continue;
6065
6081
  }
6066
- const usage = objectField(metricsRaw, "usage");
6067
- const inputTokens = numberField(usage, "inputTokens") || 0;
6068
- const outputTokens = numberField(usage, "outputTokens") || 0;
6069
- const cacheReadTokens = numberField(usage, "cacheReadTokens") || 0;
6070
- const cacheWriteTokens = numberField(usage, "cacheWriteTokens") || 0;
6071
- const reasoningTokens = numberField(usage, "reasoningTokens") || 0;
6072
- const total = inputTokens + outputTokens;
6073
- if (total <= 0) {
6082
+ const next = readShutdownSnapshot(metricsRaw);
6083
+ if (next.input + next.output <= 0) {
6084
+ continue;
6085
+ }
6086
+ const delta = positiveShutdownDelta(lastShutdownUsage.get(modelName), next);
6087
+ if (!delta) {
6074
6088
  continue;
6075
6089
  }
6076
- const requests = objectField(metricsRaw, "requests");
6077
- const requestCount = numberField(requests, "count");
6078
6090
  events.push(baseCopilotEvent({
6079
6091
  ts,
6080
6092
  type: "model.usage",
@@ -6083,38 +6095,100 @@ async function parseCopilotSessionFile(filePath, options) {
6083
6095
  project,
6084
6096
  model: modelName,
6085
6097
  confidence: "exact",
6086
- metrics: {
6087
- tokensInput: inputTokens || void 0,
6088
- tokensOutput: outputTokens || void 0,
6089
- tokensCachedInput: cacheReadTokens + cacheWriteTokens || void 0,
6090
- tokensCacheReadInput: cacheReadTokens || void 0,
6091
- tokensCacheCreationInput: cacheWriteTokens || void 0,
6092
- tokensReasoningOutput: reasoningTokens || void 0,
6093
- reasoningEffort,
6094
- tokensTotal: total,
6095
- modelCalls: requestCount || void 0
6096
- }
6098
+ metrics: shutdownUsageMetrics(delta === "full" ? next : delta, reasoningEffort, delta === "full")
6097
6099
  }));
6100
+ lastShutdownUsage.set(modelName, next);
6098
6101
  emittedShutdownUsage = true;
6099
6102
  }
6100
- events.push(baseCopilotEvent({
6101
- ts,
6102
- type: "session.ended",
6103
- sessionId,
6104
- cwd,
6105
- project,
6106
- model,
6107
- operation: "session end"
6108
- }));
6103
+ if (sawWorkSinceShutdown) {
6104
+ events.push(baseCopilotEvent({
6105
+ ts,
6106
+ type: "session.ended",
6107
+ sessionId,
6108
+ cwd,
6109
+ project,
6110
+ model,
6111
+ operation: "session end"
6112
+ }));
6113
+ sawWorkSinceShutdown = false;
6114
+ }
6109
6115
  break;
6110
6116
  }
6111
6117
  }
6112
6118
  }
6119
+ if (isTurnIdle(currentTurnLastEventAt)) {
6120
+ closeOpenTurn();
6121
+ }
6113
6122
  if (!emittedShutdownUsage) {
6114
- events.push(...pendingTurnUsage);
6123
+ for (const [turnId, accum] of turnTokenAccum) {
6124
+ if (!accum.metrics.tokensOutput) {
6125
+ continue;
6126
+ }
6127
+ accum.metrics.reasoningEffort = reasoningEffort;
6128
+ events.push(baseCopilotEvent({
6129
+ ts: accum.ts,
6130
+ type: "model.usage",
6131
+ sessionId,
6132
+ turnId,
6133
+ cwd,
6134
+ project,
6135
+ model,
6136
+ confidence: "partial",
6137
+ metrics: accum.metrics
6138
+ }));
6139
+ }
6115
6140
  }
6116
6141
  return events.filter((event) => validateCanonicalEvent(event).valid);
6117
6142
  }
6143
+ function readShutdownSnapshot(metricsRaw) {
6144
+ const usage = objectField(metricsRaw, "usage");
6145
+ const requests = objectField(metricsRaw, "requests");
6146
+ return {
6147
+ input: numberField(usage, "inputTokens") || 0,
6148
+ output: numberField(usage, "outputTokens") || 0,
6149
+ cacheRead: numberField(usage, "cacheReadTokens") || 0,
6150
+ cacheWrite: numberField(usage, "cacheWriteTokens") || 0,
6151
+ reasoning: numberField(usage, "reasoningTokens") || 0,
6152
+ requests: numberField(requests, "count") || 0
6153
+ };
6154
+ }
6155
+ function positiveShutdownDelta(prev, next) {
6156
+ if (!prev) {
6157
+ return "full";
6158
+ }
6159
+ const delta = {
6160
+ input: next.input - prev.input,
6161
+ output: next.output - prev.output,
6162
+ cacheRead: next.cacheRead - prev.cacheRead,
6163
+ cacheWrite: next.cacheWrite - prev.cacheWrite,
6164
+ reasoning: next.reasoning - prev.reasoning,
6165
+ requests: next.requests - prev.requests
6166
+ };
6167
+ if (delta.input <= 0 && delta.output <= 0 && delta.cacheRead <= 0 && delta.cacheWrite <= 0 && delta.requests <= 0) {
6168
+ return null;
6169
+ }
6170
+ return delta;
6171
+ }
6172
+ function shutdownUsageMetrics(snapshot, reasoningEffort, full) {
6173
+ const input = full ? snapshot.input : Math.max(0, snapshot.input);
6174
+ const output = full ? snapshot.output : Math.max(0, snapshot.output);
6175
+ const cacheRead = full ? snapshot.cacheRead : Math.max(0, snapshot.cacheRead);
6176
+ const cacheWrite = full ? snapshot.cacheWrite : Math.max(0, snapshot.cacheWrite);
6177
+ const reasoning = full ? snapshot.reasoning : Math.max(0, snapshot.reasoning);
6178
+ const requests = full ? snapshot.requests : Math.max(0, snapshot.requests);
6179
+ const total = input + output;
6180
+ return {
6181
+ tokensInput: input > 0 ? input : void 0,
6182
+ tokensOutput: output > 0 ? output : void 0,
6183
+ tokensCachedInput: cacheRead + cacheWrite > 0 ? cacheRead + cacheWrite : void 0,
6184
+ tokensCacheReadInput: cacheRead > 0 ? cacheRead : void 0,
6185
+ tokensCacheCreationInput: cacheWrite > 0 ? cacheWrite : void 0,
6186
+ tokensReasoningOutput: reasoning > 0 ? reasoning : void 0,
6187
+ reasoningEffort,
6188
+ tokensTotal: total > 0 ? total : void 0,
6189
+ modelCalls: requests > 0 ? requests : void 0
6190
+ };
6191
+ }
6118
6192
  function copilotFileActivities(tool, args, ts, cwd) {
6119
6193
  const normalized = tool.toLowerCase();
6120
6194
  if (normalized === "apply_patch" && typeof args === "string") {
@@ -6217,6 +6291,7 @@ function createCopilotAdapter() {
6217
6291
  }
6218
6292
 
6219
6293
  // src/adapters/cursor.ts
6294
+ import { readFileSync } from "node:fs";
6220
6295
  import { copyFile, mkdir as mkdir4, readdir as readdir6, readFile as readFile8, stat as stat8, writeFile as writeFile4 } from "node:fs/promises";
6221
6296
  import os6 from "node:os";
6222
6297
  import path13 from "node:path";
@@ -6799,9 +6874,18 @@ function cloudUsageToPersisted(row, index = 0) {
6799
6874
  tokensCacheCreationInput: row.tokensCacheCreationInput || void 0
6800
6875
  };
6801
6876
  }
6802
- async function resolveCursorSessionUsage(options, sessionId) {
6877
+ async function resolveCursorSessionUsage(options, sessionId, filePath) {
6803
6878
  const persisted = await readPersistedSessionContextFromOptions(options, sessionId);
6804
- return persisted?.usage ?? [];
6879
+ const hookUsage = persisted?.usage ?? [];
6880
+ if (hookUsage.length > 0) {
6881
+ return hookUsage;
6882
+ }
6883
+ const cloudMap = await loadCursorCloudUsageMap(options, filePath);
6884
+ const cloudEvents = cloudMap.get(sessionId);
6885
+ if (!cloudEvents?.length) {
6886
+ return [];
6887
+ }
6888
+ return cloudEvents.map((event, index) => cloudUsageToPersisted(event, index));
6805
6889
  }
6806
6890
  function cursorCloudUsageCachePath(home) {
6807
6891
  return path13.join(home, ".vibetime", CURSOR_CLOUD_USAGE_FILENAME);
@@ -7072,13 +7156,99 @@ function loadBubbles(db, composerId) {
7072
7156
  });
7073
7157
  return bubbles;
7074
7158
  }
7159
+ function pathFromCursorResource(value) {
7160
+ const trimmed = value.trim();
7161
+ if (!trimmed) {
7162
+ return void 0;
7163
+ }
7164
+ const remote = trimmed.match(/^vscode-remote:\/\/[^/]+(\/.*)$/i);
7165
+ if (remote?.[1]) {
7166
+ try {
7167
+ return decodeURIComponent(remote[1]);
7168
+ } catch {
7169
+ return remote[1];
7170
+ }
7171
+ }
7172
+ if (trimmed.startsWith("file:")) {
7173
+ const rest = trimmed.slice("file://".length);
7174
+ try {
7175
+ return decodeURIComponent(rest);
7176
+ } catch {
7177
+ return rest;
7178
+ }
7179
+ }
7180
+ if (path13.isAbsolute(trimmed) || trimmed.startsWith("/")) {
7181
+ return trimmed;
7182
+ }
7183
+ return void 0;
7184
+ }
7185
+ function projectFromFolderPaths(folders, fallback) {
7186
+ const names = [...new Set(
7187
+ folders.map((folder) => path13.basename(folder.replace(/[\\/]+$/, ""))).filter(Boolean)
7188
+ )];
7189
+ if (names.length === 1) {
7190
+ return names[0];
7191
+ }
7192
+ if (names.length > 1) {
7193
+ return names.join("+");
7194
+ }
7195
+ return fallback;
7196
+ }
7197
+ function projectFallbackFromWorkspaceFile(filePath) {
7198
+ const base = path13.basename(filePath, ".code-workspace");
7199
+ const stripped = base.replace(/-workspace$/i, "").trim();
7200
+ return stripped || void 0;
7201
+ }
7202
+ function folderPathsFromCodeWorkspace(workspacePath) {
7203
+ let parsed;
7204
+ try {
7205
+ parsed = JSON.parse(readFileSync(workspacePath, "utf8"));
7206
+ } catch {
7207
+ return [];
7208
+ }
7209
+ if (!isPlainObject(parsed) || !Array.isArray(parsed.folders)) {
7210
+ return [];
7211
+ }
7212
+ const root = path13.dirname(workspacePath);
7213
+ const folders = [];
7214
+ for (const folder of parsed.folders) {
7215
+ if (!isPlainObject(folder)) {
7216
+ continue;
7217
+ }
7218
+ const uri = stringField(folder, "uri");
7219
+ if (uri) {
7220
+ const resolved = pathFromCursorResource(uri);
7221
+ if (resolved) {
7222
+ folders.push(resolved);
7223
+ }
7224
+ continue;
7225
+ }
7226
+ const relative = stringField(folder, "path");
7227
+ if (relative) {
7228
+ folders.push(path13.resolve(root, relative));
7229
+ }
7230
+ }
7231
+ return folders;
7232
+ }
7075
7233
  function workspaceFromComposer(data) {
7076
7234
  const ident = objectField(data, "workspaceIdentifier");
7077
7235
  const uri = objectField(ident, "uri");
7078
- const cwd = stringField(uri, "fsPath") || stringField(uri, "path");
7236
+ const fromUri = stringField(uri, "fsPath") || stringField(uri, "path") || (stringField(uri, "external") ? pathFromCursorResource(stringField(uri, "external")) : void 0);
7237
+ if (fromUri) {
7238
+ return {
7239
+ cwd: fromUri,
7240
+ project: path13.basename(fromUri.replace(/[\\/]+$/, "")) || void 0
7241
+ };
7242
+ }
7243
+ const configPath2 = objectField(ident, "configPath");
7244
+ const workspaceFile = stringField(configPath2, "fsPath") || stringField(configPath2, "path") || (stringField(configPath2, "external") ? pathFromCursorResource(stringField(configPath2, "external")) : void 0);
7245
+ if (!workspaceFile) {
7246
+ return {};
7247
+ }
7248
+ const folders = folderPathsFromCodeWorkspace(workspaceFile);
7079
7249
  return {
7080
- cwd,
7081
- project: cwd ? path13.basename(cwd) : void 0
7250
+ cwd: folders[0],
7251
+ project: projectFromFolderPaths(folders, projectFallbackFromWorkspaceFile(workspaceFile))
7082
7252
  };
7083
7253
  }
7084
7254
  function modelFromComposer(data) {
@@ -7614,6 +7784,7 @@ async function parseCursorTranscriptFile(filePath, options) {
7614
7784
  push,
7615
7785
  sessionId,
7616
7786
  options,
7787
+ filePath,
7617
7788
  fallbackTs: endedAt || lastTs,
7618
7789
  lastTurnId,
7619
7790
  model,
@@ -7627,7 +7798,7 @@ async function appendPersistedCursorUsage(args) {
7627
7798
  }
7628
7799
  emitPersistedCursorUsage(
7629
7800
  args.push,
7630
- await resolveCursorSessionUsage(args.options, args.sessionId),
7801
+ await resolveCursorSessionUsage(args.options, args.sessionId, args.filePath),
7631
7802
  args.fallbackTs,
7632
7803
  args.lastTurnId,
7633
7804
  args.model,
@@ -7731,7 +7902,7 @@ async function parseCursorSessionFile(filePath, options) {
7731
7902
  try {
7732
7903
  const composers = listComposers(opened.db);
7733
7904
  for (const composer of composers) {
7734
- const usage = await resolveCursorSessionUsage(options, composer.composerId);
7905
+ const usage = await resolveCursorSessionUsage(options, composer.composerId, filePath);
7735
7906
  events.push(...parseComposer(opened.db, composer, filePath, sourcePathHash, options, usage));
7736
7907
  }
7737
7908
  } finally {
@@ -8106,7 +8277,7 @@ async function grokBotBackfillFiles(sourceRoot, home = os7.homedir(), env, optio
8106
8277
  function createGrokBotAdapter() {
8107
8278
  return {
8108
8279
  id: SOURCE_ID2,
8109
- label: "Grok Bot",
8280
+ label: "Cloud Agent",
8110
8281
  agentName: AGENT_NAME2,
8111
8282
  kind: "agent",
8112
8283
  // No hooks to install — data arrives via the Cursor dashboard API.
@@ -15026,7 +15197,7 @@ async function uninstallGeneratedFile(filePath, { dryRun, onWrite }) {
15026
15197
 
15027
15198
  // src/lib/config.ts
15028
15199
  import { randomUUID } from "node:crypto";
15029
- import { existsSync as existsSync2, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
15200
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
15030
15201
  import { homedir, hostname } from "node:os";
15031
15202
  import path24 from "node:path";
15032
15203
  function disabledBackfillSourceSet(config) {
@@ -15048,7 +15219,7 @@ function readConfig(home = homedir()) {
15048
15219
  return {};
15049
15220
  }
15050
15221
  try {
15051
- const parsed = JSON.parse(readFileSync(file, "utf8"));
15222
+ const parsed = JSON.parse(readFileSync2(file, "utf8"));
15052
15223
  return parsed && typeof parsed === "object" ? parsed : {};
15053
15224
  } catch {
15054
15225
  return {};
@@ -15065,7 +15236,7 @@ function writeConfig(config, home = homedir()) {
15065
15236
  function ensureLocalMachineId(home = homedir()) {
15066
15237
  const file = machineIdPath(home);
15067
15238
  if (existsSync2(file)) {
15068
- const value = readFileSync(file, "utf8").trim();
15239
+ const value = readFileSync2(file, "utf8").trim();
15069
15240
  if (value.length > 0) {
15070
15241
  return value;
15071
15242
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yhong91/vibetime",
3
3
  "type": "module",
4
- "version": "0.1.65",
4
+ "version": "0.1.67",
5
5
  "description": "vibetime CLI — install AI-agent hooks (Claude Code, Codex, OpenCode, Pi, Cursor) and report activity to vibetime.",
6
6
  "license": "MIT",
7
7
  "publishConfig": {