@yhong91/vibetime 0.1.52 → 0.1.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/vibetime.mjs +633 -59
- package/package.json +1 -1
package/bin/vibetime.mjs
CHANGED
|
@@ -18,7 +18,7 @@ var __export = (target, all) => {
|
|
|
18
18
|
// src/lib/fs.ts
|
|
19
19
|
var fs_exports = {};
|
|
20
20
|
__export(fs_exports, {
|
|
21
|
-
GENERATED_MARKER: () =>
|
|
21
|
+
GENERATED_MARKER: () => GENERATED_MARKER2,
|
|
22
22
|
countDirectoryEntries: () => countDirectoryEntries,
|
|
23
23
|
listFilesByExtensions: () => listFilesByExtensions,
|
|
24
24
|
listJsonlFiles: () => listJsonlFiles,
|
|
@@ -72,7 +72,7 @@ async function writeGeneratedFile(filePath, content, { dryRun, force, onWrite })
|
|
|
72
72
|
onWrite(`Already installed ${filePath}`);
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
|
-
if (existing !== null && !existing.includes(
|
|
75
|
+
if (existing !== null && !existing.includes(GENERATED_MARKER2) && !existing.includes(LEGACY_GENERATED_MARKER) && !force) {
|
|
76
76
|
throw new Error(
|
|
77
77
|
`Refusing to overwrite non-vibetime file: ${filePath}. Re-run with --force if this is intentional.`
|
|
78
78
|
);
|
|
@@ -148,11 +148,11 @@ async function countDirectoryEntries(candidatePath) {
|
|
|
148
148
|
throw error;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
var
|
|
151
|
+
var GENERATED_MARKER2, LEGACY_GENERATED_MARKER;
|
|
152
152
|
var init_fs = __esm({
|
|
153
153
|
"src/lib/fs.ts"() {
|
|
154
154
|
"use strict";
|
|
155
|
-
|
|
155
|
+
GENERATED_MARKER2 = "Generated by vibetime.";
|
|
156
156
|
LEGACY_GENERATED_MARKER = "Generated by codetime.";
|
|
157
157
|
}
|
|
158
158
|
});
|
|
@@ -884,8 +884,8 @@ var init_esm = __esm({
|
|
|
884
884
|
|
|
885
885
|
// src/cli.ts
|
|
886
886
|
import { spawn as spawn2, spawnSync } from "node:child_process";
|
|
887
|
-
import { mkdir as mkdir5, open, rm, stat as
|
|
888
|
-
import
|
|
887
|
+
import { mkdir as mkdir5, open, rm, stat as stat14, writeFile as writeFile4 } from "node:fs/promises";
|
|
888
|
+
import os12 from "node:os";
|
|
889
889
|
import path25 from "node:path";
|
|
890
890
|
import { fileURLToPath } from "node:url";
|
|
891
891
|
|
|
@@ -2047,7 +2047,8 @@ function claudeStyleFileMetrics(tool, input) {
|
|
|
2047
2047
|
}
|
|
2048
2048
|
|
|
2049
2049
|
// src/lib/constants.ts
|
|
2050
|
-
var PACKAGE_VERSION = true ? "0.1.
|
|
2050
|
+
var PACKAGE_VERSION = true ? "0.1.54" : "0.1.1";
|
|
2051
|
+
var GENERATED_MARKER = "Generated by vibetime.";
|
|
2051
2052
|
var DEFAULT_API_URL = "http://121.196.224.82:3001";
|
|
2052
2053
|
var DEFAULT_BACKFILL_BATCH_SIZE = 50;
|
|
2053
2054
|
var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
|
|
@@ -3330,6 +3331,9 @@ function isTurnIdle(lastEventAt) {
|
|
|
3330
3331
|
}
|
|
3331
3332
|
return Date.now() - lastMs > TURN_IDLE_MS;
|
|
3332
3333
|
}
|
|
3334
|
+
function withoutSubagentTurnEvents(events) {
|
|
3335
|
+
return events.filter((event) => event.type !== "turn.started" && event.type !== "turn.completed");
|
|
3336
|
+
}
|
|
3333
3337
|
function sessionIdFromFilePath(filePath, prefix) {
|
|
3334
3338
|
const match = path6.basename(filePath).match(/([0-9a-f]{8}-[0-9a-f-]{27,})/);
|
|
3335
3339
|
return match?.[1] || `${prefix}_${createStableHash(filePath).slice(0, 24)}`;
|
|
@@ -4252,7 +4256,8 @@ async function parseClaudeCoworkSessionFile(filePath, options) {
|
|
|
4252
4256
|
const parentSourcePathHash = subagentInfo ? `sha256:${createStableHash(subagentInfo.mainTranscriptPath)}` : void 0;
|
|
4253
4257
|
const parentSessionId = subagentInfo ? prefixCoworkId(subagentInfo.cliSessionId) : void 0;
|
|
4254
4258
|
const events = await parser(filePath, options);
|
|
4255
|
-
|
|
4259
|
+
const foldable = subagentInfo ? withoutSubagentTurnEvents(events).map((event) => ({ ...event, turnId: void 0 })) : events;
|
|
4260
|
+
return foldable.map((event) => {
|
|
4256
4261
|
const sessionId = parentSessionId || prefixCoworkId(event.sessionId);
|
|
4257
4262
|
const turnId = prefixCoworkId(event.turnId);
|
|
4258
4263
|
const workspaceId = createWorkspaceId({ projectName: project, repoRoot: cwd });
|
|
@@ -5006,8 +5011,8 @@ async function codebuddyBackfillFiles(sourceRoot, home, env) {
|
|
|
5006
5011
|
}
|
|
5007
5012
|
const filePath = path9.join(traceDir, entry);
|
|
5008
5013
|
try {
|
|
5009
|
-
const
|
|
5010
|
-
files.push({ path: filePath, modifiedAt:
|
|
5014
|
+
const stat15 = await import("node:fs/promises").then((fs) => fs.stat(filePath));
|
|
5015
|
+
files.push({ path: filePath, modifiedAt: stat15.mtime.toISOString(), groupId: pidDir.name });
|
|
5011
5016
|
} catch {
|
|
5012
5017
|
}
|
|
5013
5018
|
}
|
|
@@ -6938,6 +6943,154 @@ function createGrokBuildAdapter() {
|
|
|
6938
6943
|
// src/adapters/kimi-code.ts
|
|
6939
6944
|
import { readFile as readFile9 } from "node:fs/promises";
|
|
6940
6945
|
import path14 from "node:path";
|
|
6946
|
+
|
|
6947
|
+
// src/lib/toml-hooks.ts
|
|
6948
|
+
init_fs();
|
|
6949
|
+
async function hasTomlHookCommand(filePath, command) {
|
|
6950
|
+
const text = await readTextIfExists(filePath);
|
|
6951
|
+
if (!text) {
|
|
6952
|
+
return false;
|
|
6953
|
+
}
|
|
6954
|
+
return parseTomlHookRules(text).some((rule) => rule.command === command);
|
|
6955
|
+
}
|
|
6956
|
+
function parseTomlHookRules(text) {
|
|
6957
|
+
const rules = [];
|
|
6958
|
+
const chunks = text.split(/^\[\[hooks\]\][ \t]*\r?\n?/m);
|
|
6959
|
+
for (const chunk of chunks.slice(1)) {
|
|
6960
|
+
const body = chunk.split(/^\[[^\]]+\]/m)[0] ?? "";
|
|
6961
|
+
const event = tomlStringField(body, "event");
|
|
6962
|
+
const command = tomlStringField(body, "command");
|
|
6963
|
+
if (!event || !command) {
|
|
6964
|
+
continue;
|
|
6965
|
+
}
|
|
6966
|
+
const matcher = tomlStringField(body, "matcher");
|
|
6967
|
+
const timeout = tomlNumberField(body, "timeout");
|
|
6968
|
+
const rule = { event, command };
|
|
6969
|
+
if (matcher !== void 0) {
|
|
6970
|
+
rule.matcher = matcher;
|
|
6971
|
+
}
|
|
6972
|
+
if (timeout !== void 0) {
|
|
6973
|
+
rule.timeout = timeout;
|
|
6974
|
+
}
|
|
6975
|
+
rules.push(rule);
|
|
6976
|
+
}
|
|
6977
|
+
return rules;
|
|
6978
|
+
}
|
|
6979
|
+
function mergeTomlHookRules(existingText, desired) {
|
|
6980
|
+
const existingKeys = new Set(parseTomlHookRules(existingText).map(ruleKey));
|
|
6981
|
+
const toAppend = desired.filter((rule) => !existingKeys.has(ruleKey(rule)));
|
|
6982
|
+
if (toAppend.length === 0) {
|
|
6983
|
+
return existingText;
|
|
6984
|
+
}
|
|
6985
|
+
let base = existingText;
|
|
6986
|
+
if (base.length > 0 && !base.endsWith("\n")) {
|
|
6987
|
+
base += "\n";
|
|
6988
|
+
}
|
|
6989
|
+
if (base.length > 0 && !base.endsWith("\n\n") && base.trim().length > 0) {
|
|
6990
|
+
base += "\n";
|
|
6991
|
+
}
|
|
6992
|
+
const blocks = toAppend.map(serializeTomlHookRule).join("\n\n");
|
|
6993
|
+
return `${base}${blocks}
|
|
6994
|
+
`;
|
|
6995
|
+
}
|
|
6996
|
+
function removeTomlHookRulesByCommand(existingText, commands) {
|
|
6997
|
+
const commandSet = new Set(commands.filter(Boolean));
|
|
6998
|
+
if (commandSet.size === 0 || !existingText) {
|
|
6999
|
+
return existingText;
|
|
7000
|
+
}
|
|
7001
|
+
const parts = [];
|
|
7002
|
+
let cursor = 0;
|
|
7003
|
+
const headerRe = /^\[\[hooks\]\][ \t]*\r?\n?/gm;
|
|
7004
|
+
let match = headerRe.exec(existingText);
|
|
7005
|
+
let removed = 0;
|
|
7006
|
+
while (match) {
|
|
7007
|
+
const headerStart = match.index;
|
|
7008
|
+
const bodyStart = headerRe.lastIndex;
|
|
7009
|
+
const rest = existingText.slice(bodyStart);
|
|
7010
|
+
const nextHeader = rest.search(/^\[[^\]]+\]/m);
|
|
7011
|
+
const bodyEnd = nextHeader === -1 ? existingText.length : bodyStart + nextHeader;
|
|
7012
|
+
const body = existingText.slice(bodyStart, bodyEnd);
|
|
7013
|
+
const command = tomlStringField(body, "command");
|
|
7014
|
+
parts.push(existingText.slice(cursor, headerStart));
|
|
7015
|
+
if (command && commandSet.has(command)) {
|
|
7016
|
+
removed += 1;
|
|
7017
|
+
} else {
|
|
7018
|
+
parts.push(existingText.slice(headerStart, bodyEnd));
|
|
7019
|
+
}
|
|
7020
|
+
cursor = bodyEnd;
|
|
7021
|
+
match = headerRe.exec(existingText);
|
|
7022
|
+
}
|
|
7023
|
+
if (removed === 0) {
|
|
7024
|
+
return existingText;
|
|
7025
|
+
}
|
|
7026
|
+
parts.push(existingText.slice(cursor));
|
|
7027
|
+
let next = parts.join("");
|
|
7028
|
+
next = next.replace(/\n{3,}/g, "\n\n");
|
|
7029
|
+
if (next.length > 0 && !next.endsWith("\n")) {
|
|
7030
|
+
next += "\n";
|
|
7031
|
+
}
|
|
7032
|
+
return next;
|
|
7033
|
+
}
|
|
7034
|
+
function looksLikeToml(text) {
|
|
7035
|
+
const sample = text.trim();
|
|
7036
|
+
if (!sample) {
|
|
7037
|
+
return true;
|
|
7038
|
+
}
|
|
7039
|
+
if (sample.startsWith("{")) {
|
|
7040
|
+
return false;
|
|
7041
|
+
}
|
|
7042
|
+
if (/^\s*\[\s*\{/.test(sample)) {
|
|
7043
|
+
return false;
|
|
7044
|
+
}
|
|
7045
|
+
return true;
|
|
7046
|
+
}
|
|
7047
|
+
function ruleKey(rule) {
|
|
7048
|
+
return `${rule.event}\0${rule.command}`;
|
|
7049
|
+
}
|
|
7050
|
+
function serializeTomlHookRule(rule) {
|
|
7051
|
+
const lines = [
|
|
7052
|
+
"[[hooks]]",
|
|
7053
|
+
`event = "${escapeTomlString(rule.event)}"`
|
|
7054
|
+
];
|
|
7055
|
+
if (rule.matcher !== void 0 && rule.matcher !== "") {
|
|
7056
|
+
lines.push(`matcher = "${escapeTomlString(rule.matcher)}"`);
|
|
7057
|
+
}
|
|
7058
|
+
lines.push(`command = "${escapeTomlString(rule.command)}"`);
|
|
7059
|
+
if (rule.timeout !== void 0) {
|
|
7060
|
+
lines.push(`timeout = ${Math.floor(rule.timeout)}`);
|
|
7061
|
+
}
|
|
7062
|
+
return lines.join("\n");
|
|
7063
|
+
}
|
|
7064
|
+
function escapeTomlString(value) {
|
|
7065
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\n", "\\n").replaceAll(" ", "\\t");
|
|
7066
|
+
}
|
|
7067
|
+
function tomlStringField(body, key) {
|
|
7068
|
+
const re = new RegExp(
|
|
7069
|
+
`^\\s*${key}\\s*=\\s*(?:"((?:\\\\.|[^"\\\\])*)"|'([^']*)')\\s*$`,
|
|
7070
|
+
"m"
|
|
7071
|
+
);
|
|
7072
|
+
const match = body.match(re);
|
|
7073
|
+
if (!match) {
|
|
7074
|
+
return void 0;
|
|
7075
|
+
}
|
|
7076
|
+
if (match[1] !== void 0) {
|
|
7077
|
+
return match[1].replaceAll("\\n", "\n").replaceAll("\\t", " ").replaceAll('\\"', '"').replaceAll("\\\\", "\\");
|
|
7078
|
+
}
|
|
7079
|
+
return match[2];
|
|
7080
|
+
}
|
|
7081
|
+
function tomlNumberField(body, key) {
|
|
7082
|
+
const re = new RegExp(`^\\s*${key}\\s*=\\s*(-?\\d+)\\s*$`, "m");
|
|
7083
|
+
const match = body.match(re);
|
|
7084
|
+
if (!match?.[1]) {
|
|
7085
|
+
return void 0;
|
|
7086
|
+
}
|
|
7087
|
+
const n = Number.parseInt(match[1], 10);
|
|
7088
|
+
return Number.isFinite(n) ? n : void 0;
|
|
7089
|
+
}
|
|
7090
|
+
|
|
7091
|
+
// src/adapters/kimi-code.ts
|
|
7092
|
+
var HOOK_COMMAND2 = "vibetime hook --agent kimi-code";
|
|
7093
|
+
var HOOK_TIMEOUT_SECONDS = 10;
|
|
6941
7094
|
function kimiCodeHome(home, env) {
|
|
6942
7095
|
const override = env?.KIMI_CODE_HOME || env?.KIMI_HOME;
|
|
6943
7096
|
if (override && override.trim()) {
|
|
@@ -6948,6 +7101,28 @@ function kimiCodeHome(home, env) {
|
|
|
6948
7101
|
function kimiCodeSessionsDir(home, env) {
|
|
6949
7102
|
return path14.join(kimiCodeHome(home, env), "sessions");
|
|
6950
7103
|
}
|
|
7104
|
+
function kimiCodeConfigPath(home, env) {
|
|
7105
|
+
return path14.join(kimiCodeHome(home, env), "config.toml");
|
|
7106
|
+
}
|
|
7107
|
+
function kimiHookRules() {
|
|
7108
|
+
const events = [
|
|
7109
|
+
"SessionStart",
|
|
7110
|
+
"SessionEnd",
|
|
7111
|
+
"UserPromptSubmit",
|
|
7112
|
+
"TurnStarted",
|
|
7113
|
+
"PostToolUse",
|
|
7114
|
+
"PostToolUseFailure",
|
|
7115
|
+
"Stop",
|
|
7116
|
+
"StopFailure",
|
|
7117
|
+
"PermissionResult",
|
|
7118
|
+
"SubagentStop"
|
|
7119
|
+
];
|
|
7120
|
+
return events.map((event) => ({
|
|
7121
|
+
event,
|
|
7122
|
+
command: HOOK_COMMAND2,
|
|
7123
|
+
timeout: HOOK_TIMEOUT_SECONDS
|
|
7124
|
+
}));
|
|
7125
|
+
}
|
|
6951
7126
|
function baseKimiEvent(event) {
|
|
6952
7127
|
return {
|
|
6953
7128
|
schemaVersion: AGENT_TIME_SCHEMA_VERSION,
|
|
@@ -7387,13 +7562,17 @@ function createKimiCodeAdapter() {
|
|
|
7387
7562
|
return kimiCodeHome(home, env);
|
|
7388
7563
|
},
|
|
7389
7564
|
installedPath(home, env) {
|
|
7390
|
-
return
|
|
7565
|
+
return kimiCodeConfigPath(home, env);
|
|
7391
7566
|
},
|
|
7392
|
-
async isInstalled() {
|
|
7393
|
-
return
|
|
7567
|
+
async isInstalled(home, env) {
|
|
7568
|
+
return hasTomlHookCommand(kimiCodeConfigPath(home, env), HOOK_COMMAND2);
|
|
7394
7569
|
},
|
|
7395
|
-
installEntries(
|
|
7396
|
-
return [
|
|
7570
|
+
installEntries(home, env) {
|
|
7571
|
+
return [{
|
|
7572
|
+
kind: "hooks-toml",
|
|
7573
|
+
path: kimiCodeConfigPath(home, env),
|
|
7574
|
+
content: { hooks: kimiHookRules() }
|
|
7575
|
+
}];
|
|
7397
7576
|
},
|
|
7398
7577
|
sourcePaths(home, env) {
|
|
7399
7578
|
return [kimiCodeSessionsDir(home, env)];
|
|
@@ -7460,6 +7639,8 @@ async function parseOpenCodeSessionFile(dbPath, options) {
|
|
|
7460
7639
|
for (const session of sessions) {
|
|
7461
7640
|
const rawSessionId = session.id;
|
|
7462
7641
|
const sessionId = rootIdByRawId.get(rawSessionId) || rawSessionId;
|
|
7642
|
+
const isSubagent = sessionId !== rawSessionId;
|
|
7643
|
+
const sessionEventStart = events.length;
|
|
7463
7644
|
const cwd = session.directory || session.path || void 0;
|
|
7464
7645
|
const project = cwd ? path15.basename(cwd) : void 0;
|
|
7465
7646
|
const sessionTs = msToIso(session.time_created);
|
|
@@ -7776,6 +7957,9 @@ async function parseOpenCodeSessionFile(dbPath, options) {
|
|
|
7776
7957
|
operation: "session end"
|
|
7777
7958
|
}));
|
|
7778
7959
|
}
|
|
7960
|
+
if (isSubagent) {
|
|
7961
|
+
events.push(...withoutSubagentTurnEvents(events.splice(sessionEventStart)).map((event) => ({ ...event, turnId: void 0 })));
|
|
7962
|
+
}
|
|
7779
7963
|
}
|
|
7780
7964
|
} finally {
|
|
7781
7965
|
db.close();
|
|
@@ -7854,19 +8038,19 @@ function opencodeDataCandidates(home, env) {
|
|
|
7854
8038
|
return [primary, path15.join(home, ".opencode", "opencode.db")];
|
|
7855
8039
|
}
|
|
7856
8040
|
async function opencodeBackfillFiles(sourceRoot, home = os6.homedir(), env) {
|
|
7857
|
-
const { stat:
|
|
8041
|
+
const { stat: stat15 } = await import("node:fs/promises");
|
|
7858
8042
|
if (sourceRoot) {
|
|
7859
8043
|
if (!sourceRoot.endsWith(".db")) {
|
|
7860
8044
|
return [];
|
|
7861
8045
|
}
|
|
7862
|
-
const info = await
|
|
8046
|
+
const info = await stat15(sourceRoot).catch(() => null);
|
|
7863
8047
|
if (!info) {
|
|
7864
8048
|
return [];
|
|
7865
8049
|
}
|
|
7866
8050
|
return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
|
|
7867
8051
|
}
|
|
7868
8052
|
for (const candidatePath of opencodeDataCandidates(home, env)) {
|
|
7869
|
-
const info = await
|
|
8053
|
+
const info = await stat15(candidatePath).catch(() => null);
|
|
7870
8054
|
if (info) {
|
|
7871
8055
|
return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
|
|
7872
8056
|
}
|
|
@@ -7966,8 +8150,10 @@ function createOpenCodeAdapter() {
|
|
|
7966
8150
|
}
|
|
7967
8151
|
|
|
7968
8152
|
// src/adapters/pi.ts
|
|
7969
|
-
import { readFile as readFile10 } from "node:fs/promises";
|
|
8153
|
+
import { readdir as readdir7, readFile as readFile10, stat as stat8 } from "node:fs/promises";
|
|
8154
|
+
import os7 from "node:os";
|
|
7970
8155
|
import path16 from "node:path";
|
|
8156
|
+
init_fs();
|
|
7971
8157
|
function parsePiSubagentLink(filePath, headerParentSession) {
|
|
7972
8158
|
if (headerParentSession) {
|
|
7973
8159
|
const parentFile = path16.isAbsolute(headerParentSession) ? headerParentSession : void 0;
|
|
@@ -8012,11 +8198,15 @@ async function resolveParentContext(link, options) {
|
|
|
8012
8198
|
return void 0;
|
|
8013
8199
|
}
|
|
8014
8200
|
function foldEventsIntoParent(events, link) {
|
|
8015
|
-
const
|
|
8016
|
-
|
|
8201
|
+
const folded = withoutSubagentTurnEvents(events);
|
|
8202
|
+
const childSessionId = folded.find((event) => event.sessionId)?.sessionId;
|
|
8203
|
+
return folded.map((event) => {
|
|
8017
8204
|
const rewritten = {
|
|
8018
8205
|
...event,
|
|
8019
|
-
sessionId: link.parentSessionId
|
|
8206
|
+
sessionId: link.parentSessionId,
|
|
8207
|
+
// Subagent turns don't exist in the host session; drop the reference so
|
|
8208
|
+
// buildSessionRollups doesn't synthesize phantom turn rollups.
|
|
8209
|
+
turnId: void 0
|
|
8020
8210
|
};
|
|
8021
8211
|
if (childSessionId && childSessionId !== link.parentSessionId) {
|
|
8022
8212
|
rewritten.refs = {
|
|
@@ -8272,6 +8462,100 @@ async function parsePiSessionFile(filePath, options) {
|
|
|
8272
8462
|
}
|
|
8273
8463
|
return state.events.filter((event) => matchesBackfillFilters(event, options));
|
|
8274
8464
|
}
|
|
8465
|
+
async function parsePiFile(filePath, options) {
|
|
8466
|
+
if (filePath.endsWith(".json")) {
|
|
8467
|
+
return parsePiWorkflowRunFile(filePath, options);
|
|
8468
|
+
}
|
|
8469
|
+
return parsePiSessionFile(filePath, options);
|
|
8470
|
+
}
|
|
8471
|
+
async function parsePiWorkflowRunFile(filePath, options) {
|
|
8472
|
+
let raw;
|
|
8473
|
+
try {
|
|
8474
|
+
raw = JSON.parse(await readFile10(filePath, "utf8"));
|
|
8475
|
+
} catch {
|
|
8476
|
+
return [];
|
|
8477
|
+
}
|
|
8478
|
+
if (!isPlainObject(raw)) {
|
|
8479
|
+
return [];
|
|
8480
|
+
}
|
|
8481
|
+
const sessionId = stringField(raw, "sessionId");
|
|
8482
|
+
const agents = Array.isArray(raw.agents) ? raw.agents.filter((agent) => isPlainObject(agent)) : [];
|
|
8483
|
+
if (!sessionId || agents.length === 0) {
|
|
8484
|
+
return [];
|
|
8485
|
+
}
|
|
8486
|
+
const runUpdatedAt = timestampFrom(raw.updatedAt);
|
|
8487
|
+
const parentSessionFile = await findPiSessionFileById(sessionId, options);
|
|
8488
|
+
const host = await resolveParentContext(
|
|
8489
|
+
{ parentSessionId: sessionId, parentSessionFile, explicit: true },
|
|
8490
|
+
options
|
|
8491
|
+
);
|
|
8492
|
+
const cwd = host?.cwd;
|
|
8493
|
+
const project = host?.project;
|
|
8494
|
+
const state = new SessionParserState(filePath, options, (event) => basePiEvent({ ...event, cwd, project, model: event.model }));
|
|
8495
|
+
const push = (event, ln) => {
|
|
8496
|
+
state.push(
|
|
8497
|
+
{ ...event, workspaceId: event.workspaceId || createWorkspaceId({ projectName: project, repoRoot: cwd }) },
|
|
8498
|
+
ln,
|
|
8499
|
+
"workflow-run",
|
|
8500
|
+
event.type
|
|
8501
|
+
);
|
|
8502
|
+
};
|
|
8503
|
+
for (const [index, agent] of agents.entries()) {
|
|
8504
|
+
const lineNumber = index + 1;
|
|
8505
|
+
const usage = objectField(agent, "tokenUsage");
|
|
8506
|
+
const input = numberField(usage, "input") || 0;
|
|
8507
|
+
const output = numberField(usage, "output") || 0;
|
|
8508
|
+
const cacheRead = numberField(usage, "cacheRead") || 0;
|
|
8509
|
+
const cacheWrite = numberField(usage, "cacheWrite") || 0;
|
|
8510
|
+
const breakdown = input + output + cacheRead + cacheWrite;
|
|
8511
|
+
const total = numberField(usage, "total") || breakdown || numberField(agent, "tokens") || 0;
|
|
8512
|
+
if (total <= 0) {
|
|
8513
|
+
continue;
|
|
8514
|
+
}
|
|
8515
|
+
const model = stringField(agent, "model")?.replace(/:(off|minimal|low|medium|high|xhigh|max)$/, "");
|
|
8516
|
+
const ts = timestampFrom(agent.endedAt) || timestampFrom(agent.startedAt) || runUpdatedAt || (/* @__PURE__ */ new Date()).toISOString();
|
|
8517
|
+
push(basePiEvent({
|
|
8518
|
+
ts,
|
|
8519
|
+
type: "model.usage",
|
|
8520
|
+
sessionId,
|
|
8521
|
+
cwd,
|
|
8522
|
+
project,
|
|
8523
|
+
model,
|
|
8524
|
+
confidence: breakdown > 0 ? "exact" : "derived",
|
|
8525
|
+
metrics: {
|
|
8526
|
+
tokensInput: breakdown > 0 ? input + cacheRead + cacheWrite || void 0 : void 0,
|
|
8527
|
+
tokensOutput: breakdown > 0 ? output || void 0 : void 0,
|
|
8528
|
+
tokensCachedInput: breakdown > 0 ? cacheRead + cacheWrite || void 0 : void 0,
|
|
8529
|
+
tokensCacheReadInput: breakdown > 0 ? cacheRead || void 0 : void 0,
|
|
8530
|
+
tokensCacheCreationInput: breakdown > 0 ? cacheWrite || void 0 : void 0,
|
|
8531
|
+
tokensTotal: total,
|
|
8532
|
+
costUsd: numberField(usage, "cost") || void 0
|
|
8533
|
+
},
|
|
8534
|
+
refs: stringRefs({
|
|
8535
|
+
sourceId: stringField(agent, "callId") || stringField(agent, "label")
|
|
8536
|
+
})
|
|
8537
|
+
}), lineNumber);
|
|
8538
|
+
}
|
|
8539
|
+
return state.events.filter((event) => matchesBackfillFilters(event, options));
|
|
8540
|
+
}
|
|
8541
|
+
async function findPiSessionFileById(sessionId, options) {
|
|
8542
|
+
const sessionsDir = piSessionDir(path16.resolve(stringOption(options.home) || os7.homedir()));
|
|
8543
|
+
try {
|
|
8544
|
+
const projectDirs = await readdir7(sessionsDir, { withFileTypes: true });
|
|
8545
|
+
for (const dir of projectDirs) {
|
|
8546
|
+
if (!dir.isDirectory()) {
|
|
8547
|
+
continue;
|
|
8548
|
+
}
|
|
8549
|
+
const names = await readdir7(path16.join(sessionsDir, dir.name));
|
|
8550
|
+
const hit = names.find((name) => name.endsWith(`_${sessionId}.jsonl`));
|
|
8551
|
+
if (hit) {
|
|
8552
|
+
return path16.join(sessionsDir, dir.name, hit);
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
} catch {
|
|
8556
|
+
}
|
|
8557
|
+
return void 0;
|
|
8558
|
+
}
|
|
8275
8559
|
function basePiEvent(event) {
|
|
8276
8560
|
return {
|
|
8277
8561
|
schemaVersion: AGENT_TIME_SCHEMA_VERSION,
|
|
@@ -8518,6 +8802,20 @@ function piSessionDir(home, env) {
|
|
|
8518
8802
|
}
|
|
8519
8803
|
return path16.join(piAgentDir(home, env), "sessions");
|
|
8520
8804
|
}
|
|
8805
|
+
function piWorkflowProjectsDir(home) {
|
|
8806
|
+
return path16.join(home, ".pi", "workflows", "projects");
|
|
8807
|
+
}
|
|
8808
|
+
async function piBackfillFiles(sourceRoot, home = os7.homedir(), env) {
|
|
8809
|
+
const lists = sourceRoot ? [await listFilesByExtensions(sourceRoot, [".jsonl", ".json"])] : await Promise.all([
|
|
8810
|
+
listFilesByExtensions(piSessionDir(home, env), [".jsonl"]),
|
|
8811
|
+
listFilesByExtensions(piWorkflowProjectsDir(home), [".json"])
|
|
8812
|
+
]);
|
|
8813
|
+
const files = lists.flat().sort();
|
|
8814
|
+
return Promise.all(files.map(async (filePath) => {
|
|
8815
|
+
const info = await stat8(filePath);
|
|
8816
|
+
return { path: filePath, modifiedAt: info.mtime.toISOString() };
|
|
8817
|
+
}));
|
|
8818
|
+
}
|
|
8521
8819
|
function createPiAdapter() {
|
|
8522
8820
|
return {
|
|
8523
8821
|
id: "pi",
|
|
@@ -8546,20 +8844,20 @@ function createPiAdapter() {
|
|
|
8546
8844
|
}];
|
|
8547
8845
|
},
|
|
8548
8846
|
sourcePaths(home, env) {
|
|
8549
|
-
return [piSessionDir(home, env)];
|
|
8847
|
+
return [piSessionDir(home, env), piWorkflowProjectsDir(home)];
|
|
8550
8848
|
},
|
|
8551
|
-
parseSessionFile:
|
|
8849
|
+
parseSessionFile: parsePiFile
|
|
8552
8850
|
};
|
|
8553
8851
|
}
|
|
8554
8852
|
|
|
8555
8853
|
// src/adapters/qoder-cn.ts
|
|
8556
|
-
import { readdir as
|
|
8557
|
-
import
|
|
8854
|
+
import { readdir as readdir8, readFile as readFile11, stat as stat9 } from "node:fs/promises";
|
|
8855
|
+
import os9 from "node:os";
|
|
8558
8856
|
import path18 from "node:path";
|
|
8559
8857
|
|
|
8560
8858
|
// src/adapters/qoder-local-db.ts
|
|
8561
8859
|
import { access } from "node:fs/promises";
|
|
8562
|
-
import
|
|
8860
|
+
import os8 from "node:os";
|
|
8563
8861
|
import path17 from "node:path";
|
|
8564
8862
|
function takeQoderDbModelCall(calls, requestId, blockStart) {
|
|
8565
8863
|
if (requestId) {
|
|
@@ -8577,7 +8875,7 @@ function takeQoderDbModelCall(calls, requestId, blockStart) {
|
|
|
8577
8875
|
}
|
|
8578
8876
|
return calls.ordered.shift();
|
|
8579
8877
|
}
|
|
8580
|
-
function appDataRoot(appDirName, home =
|
|
8878
|
+
function appDataRoot(appDirName, home = os8.homedir()) {
|
|
8581
8879
|
if (process.platform === "darwin") {
|
|
8582
8880
|
return path17.join(home, "Library", "Application Support", appDirName);
|
|
8583
8881
|
}
|
|
@@ -8830,7 +9128,7 @@ async function loadQoderCnSegmentModelCalls(filePath, isSubagentSession, modelMa
|
|
|
8830
9128
|
const segmentsPath = path18.join(configDir2, "logs", "sessions", projectName, sessionId, "segments");
|
|
8831
9129
|
const modelCalls = [];
|
|
8832
9130
|
try {
|
|
8833
|
-
const files = await
|
|
9131
|
+
const files = await readdir8(segmentsPath);
|
|
8834
9132
|
for (const file of files) {
|
|
8835
9133
|
if (!file.endsWith(".jsonl")) {
|
|
8836
9134
|
continue;
|
|
@@ -8880,7 +9178,7 @@ async function parseQoderCnSessionFile(filePath, options) {
|
|
|
8880
9178
|
let cwd;
|
|
8881
9179
|
let project = projectContext.project;
|
|
8882
9180
|
let model;
|
|
8883
|
-
const home = path18.resolve(stringOption(options.home) ||
|
|
9181
|
+
const home = path18.resolve(stringOption(options.home) || os9.homedir());
|
|
8884
9182
|
const modelMap = await loadQoderCnModelNames(configDir2, home);
|
|
8885
9183
|
const isSubagentSession = filePath.includes("subagents");
|
|
8886
9184
|
const segmentModelCalls = await loadQoderCnSegmentModelCalls(filePath, isSubagentSession, modelMap);
|
|
@@ -9448,7 +9746,7 @@ async function gitRootFromCwds2(cwds) {
|
|
|
9448
9746
|
while (!seen.has(current)) {
|
|
9449
9747
|
seen.add(current);
|
|
9450
9748
|
try {
|
|
9451
|
-
await
|
|
9749
|
+
await stat9(path18.join(current, ".git"));
|
|
9452
9750
|
return current;
|
|
9453
9751
|
} catch {
|
|
9454
9752
|
}
|
|
@@ -9482,7 +9780,7 @@ function encodeQoderCnProjectPath(value) {
|
|
|
9482
9780
|
}
|
|
9483
9781
|
async function qoderCnProjectFromFilePath(filePath, options) {
|
|
9484
9782
|
const projectDir = path18.basename(path18.dirname(filePath));
|
|
9485
|
-
const home = options ? path18.resolve(stringOption(options.home) ||
|
|
9783
|
+
const home = options ? path18.resolve(stringOption(options.home) || os9.homedir()) : os9.homedir();
|
|
9486
9784
|
const resolved = await resolveQoderCnProjectPath(projectDir, home);
|
|
9487
9785
|
if (resolved) {
|
|
9488
9786
|
return path18.basename(resolved);
|
|
@@ -9505,7 +9803,7 @@ async function resolveQoderCnProjectPath(projectDir, home) {
|
|
|
9505
9803
|
while (projectDir.startsWith(`${currentEncoded}-`)) {
|
|
9506
9804
|
let matchedChild;
|
|
9507
9805
|
try {
|
|
9508
|
-
const entries = await
|
|
9806
|
+
const entries = await readdir8(current, { withFileTypes: true });
|
|
9509
9807
|
for (const entry of entries) {
|
|
9510
9808
|
if (!entry.isDirectory()) {
|
|
9511
9809
|
continue;
|
|
@@ -9598,8 +9896,8 @@ function createQoderCnAdapter() {
|
|
|
9598
9896
|
|
|
9599
9897
|
// src/adapters/qoder.ts
|
|
9600
9898
|
import { existsSync } from "node:fs";
|
|
9601
|
-
import { readdir as
|
|
9602
|
-
import
|
|
9899
|
+
import { readdir as readdir9, readFile as readFile12, stat as stat10 } from "node:fs/promises";
|
|
9900
|
+
import os10 from "node:os";
|
|
9603
9901
|
import path19 from "node:path";
|
|
9604
9902
|
function parseQoderPaths(filePath) {
|
|
9605
9903
|
const parts = filePath.split(path19.sep);
|
|
@@ -9708,7 +10006,7 @@ async function loadQoderSegmentModelCalls(filePath, isSubagentSession, modelMap)
|
|
|
9708
10006
|
const segmentsPath = path19.join(configDir2, "logs", "sessions", projectName, sessionId, "segments");
|
|
9709
10007
|
const modelCalls = [];
|
|
9710
10008
|
try {
|
|
9711
|
-
const files = await
|
|
10009
|
+
const files = await readdir9(segmentsPath);
|
|
9712
10010
|
for (const file of files) {
|
|
9713
10011
|
if (!file.endsWith(".jsonl")) {
|
|
9714
10012
|
continue;
|
|
@@ -9758,7 +10056,7 @@ async function parseQoderSessionFile(filePath, options) {
|
|
|
9758
10056
|
let cwd;
|
|
9759
10057
|
let project = projectContext.project;
|
|
9760
10058
|
let model;
|
|
9761
|
-
const home = path19.resolve(stringOption(options.home) ||
|
|
10059
|
+
const home = path19.resolve(stringOption(options.home) || os10.homedir());
|
|
9762
10060
|
const modelMap = await loadQoderModelNames(configDir2, home);
|
|
9763
10061
|
const qwenworkRoot = isQwenworkConfigRoot(configDir2);
|
|
9764
10062
|
const isSubagentSession = filePath.includes("subagents");
|
|
@@ -10292,7 +10590,7 @@ async function gitRootFromCwds3(cwds) {
|
|
|
10292
10590
|
while (!seen.has(current)) {
|
|
10293
10591
|
seen.add(current);
|
|
10294
10592
|
try {
|
|
10295
|
-
await
|
|
10593
|
+
await stat10(path19.join(current, ".git"));
|
|
10296
10594
|
return current;
|
|
10297
10595
|
} catch {
|
|
10298
10596
|
}
|
|
@@ -10344,7 +10642,7 @@ function qoderEncodedProjectSuffix(projectDir, home) {
|
|
|
10344
10642
|
}
|
|
10345
10643
|
async function qoderProjectFromFilePath(filePath, options) {
|
|
10346
10644
|
const projectDir = path19.basename(path19.dirname(filePath));
|
|
10347
|
-
const home = options ? path19.resolve(stringOption(options.home) ||
|
|
10645
|
+
const home = options ? path19.resolve(stringOption(options.home) || os10.homedir()) : os10.homedir();
|
|
10348
10646
|
const resolved = await resolveQoderProjectPath(projectDir, home);
|
|
10349
10647
|
if (resolved) {
|
|
10350
10648
|
return path19.basename(resolved);
|
|
@@ -10368,7 +10666,7 @@ async function resolveQoderProjectPath(projectDir, home) {
|
|
|
10368
10666
|
while (currentEncodedVariants.some((prefix) => projectDir.startsWith(`${prefix}-`))) {
|
|
10369
10667
|
let matchedChild;
|
|
10370
10668
|
try {
|
|
10371
|
-
const entries = await
|
|
10669
|
+
const entries = await readdir9(current, { withFileTypes: true });
|
|
10372
10670
|
for (const entry of entries) {
|
|
10373
10671
|
if (!entry.isDirectory()) {
|
|
10374
10672
|
continue;
|
|
@@ -10500,7 +10798,7 @@ function normalizeId(id) {
|
|
|
10500
10798
|
}
|
|
10501
10799
|
|
|
10502
10800
|
// src/adapters/workbuddy.ts
|
|
10503
|
-
import { readdir as
|
|
10801
|
+
import { readdir as readdir10, readFile as readFile13, stat as stat11 } from "node:fs/promises";
|
|
10504
10802
|
import path20 from "node:path";
|
|
10505
10803
|
function workbuddyProjectsDir(home, env) {
|
|
10506
10804
|
const override = env?.WORKBUDDY_PROJECTS_DIR || env?.WORKBUDDY_HOME;
|
|
@@ -10943,17 +11241,17 @@ async function workbuddyBackfillFiles(sourceRoot, home, env) {
|
|
|
10943
11241
|
const base = sourceRoot || workbuddyProjectsDir(home, env);
|
|
10944
11242
|
const files = [];
|
|
10945
11243
|
try {
|
|
10946
|
-
const projects = await
|
|
11244
|
+
const projects = await readdir10(base, { withFileTypes: true });
|
|
10947
11245
|
for (const project of projects) {
|
|
10948
11246
|
if (!project.isDirectory()) {
|
|
10949
11247
|
continue;
|
|
10950
11248
|
}
|
|
10951
11249
|
const projectDir = path20.join(base, project.name);
|
|
10952
|
-
const entries = await
|
|
11250
|
+
const entries = await readdir10(projectDir, { withFileTypes: true });
|
|
10953
11251
|
for (const entry of entries) {
|
|
10954
11252
|
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
10955
11253
|
const filePath = path20.join(projectDir, entry.name);
|
|
10956
|
-
const info = await
|
|
11254
|
+
const info = await stat11(filePath);
|
|
10957
11255
|
files.push({ path: filePath, modifiedAt: info.mtime.toISOString() });
|
|
10958
11256
|
}
|
|
10959
11257
|
}
|
|
@@ -11014,7 +11312,7 @@ function createWorkbuddyAdapter() {
|
|
|
11014
11312
|
|
|
11015
11313
|
// src/adapters/zcode.ts
|
|
11016
11314
|
import { execFile } from "node:child_process";
|
|
11017
|
-
import { readFile as readFile14, stat as
|
|
11315
|
+
import { readFile as readFile14, stat as stat12 } from "node:fs/promises";
|
|
11018
11316
|
import path21 from "node:path";
|
|
11019
11317
|
import { promisify as promisify2 } from "node:util";
|
|
11020
11318
|
init_fs();
|
|
@@ -11033,7 +11331,7 @@ var providerNameCache = null;
|
|
|
11033
11331
|
async function loadProviderNames(configPath2) {
|
|
11034
11332
|
let fileMtime = 0;
|
|
11035
11333
|
try {
|
|
11036
|
-
const info = await
|
|
11334
|
+
const info = await stat12(configPath2);
|
|
11037
11335
|
fileMtime = info.mtimeMs;
|
|
11038
11336
|
} catch {
|
|
11039
11337
|
return /* @__PURE__ */ new Map();
|
|
@@ -11233,6 +11531,7 @@ function sessionContext(row, sessions) {
|
|
|
11233
11531
|
return {
|
|
11234
11532
|
rawSessionId: sessionId,
|
|
11235
11533
|
sessionId: `zcode:${rootSessionId}`,
|
|
11534
|
+
isSubagent: rootSessionId !== sessionId,
|
|
11236
11535
|
cwd,
|
|
11237
11536
|
project,
|
|
11238
11537
|
workspaceId: createWorkspaceId({ projectName: project, repoRoot: cwd }),
|
|
@@ -11249,7 +11548,7 @@ async function parseZCodeDb(filePath, options) {
|
|
|
11249
11548
|
for (let i = 0; i < 12; i++) {
|
|
11250
11549
|
const probe = path21.join(candidate, ".zcode", "v2", "config.json");
|
|
11251
11550
|
try {
|
|
11252
|
-
await
|
|
11551
|
+
await stat12(probe);
|
|
11253
11552
|
configPath2 = probe;
|
|
11254
11553
|
break;
|
|
11255
11554
|
} catch {
|
|
@@ -11324,6 +11623,9 @@ async function parseZCodeDb(filePath, options) {
|
|
|
11324
11623
|
return;
|
|
11325
11624
|
}
|
|
11326
11625
|
const ctx = sessionContext(row, sessions);
|
|
11626
|
+
if (ctx.isSubagent) {
|
|
11627
|
+
return;
|
|
11628
|
+
}
|
|
11327
11629
|
const turnId = `zcode:${rawTurnId}`;
|
|
11328
11630
|
const started = makeEvent2({
|
|
11329
11631
|
schemaVersion: AGENT_TIME_SCHEMA_VERSION,
|
|
@@ -11384,7 +11686,8 @@ async function parseZCodeDb(filePath, options) {
|
|
|
11384
11686
|
project: ctx.project,
|
|
11385
11687
|
cwd: ctx.cwd,
|
|
11386
11688
|
sessionId: ctx.sessionId,
|
|
11387
|
-
|
|
11689
|
+
// Subagent turns are dropped; keep their events out of turn rollups.
|
|
11690
|
+
turnId: !ctx.isSubagent && stringField(row, "turn_id") ? `zcode:${stringField(row, "turn_id")}` : void 0,
|
|
11388
11691
|
agent: stringField(row, "agent") || "zcode",
|
|
11389
11692
|
provider: nameMap.get(stringField(row, "provider_id") || "") || stringField(row, "provider_id"),
|
|
11390
11693
|
model: modelId,
|
|
@@ -11410,7 +11713,7 @@ async function parseZCodeDb(filePath, options) {
|
|
|
11410
11713
|
return;
|
|
11411
11714
|
}
|
|
11412
11715
|
const ctx = sessionContext(row, sessions);
|
|
11413
|
-
const turnId = stringField(row, "turn_id") ? `zcode:${stringField(row, "turn_id")}` : void 0;
|
|
11716
|
+
const turnId = !ctx.isSubagent && stringField(row, "turn_id") ? `zcode:${stringField(row, "turn_id")}` : void 0;
|
|
11414
11717
|
const spanId = `${ctx.sessionId}:tool:${toolCallId}`;
|
|
11415
11718
|
const started = makeEvent2({
|
|
11416
11719
|
schemaVersion: AGENT_TIME_SCHEMA_VERSION,
|
|
@@ -11471,7 +11774,7 @@ async function zcodeBackfillFiles(sourceRoot, home, env) {
|
|
|
11471
11774
|
const candidate = sourceRoot || zcodeDbPath(home, env);
|
|
11472
11775
|
const filePath = candidate.endsWith(".sqlite") ? candidate : path21.join(candidate, "db", "db.sqlite");
|
|
11473
11776
|
try {
|
|
11474
|
-
const info = await
|
|
11777
|
+
const info = await stat12(filePath);
|
|
11475
11778
|
return [{ path: filePath, modifiedAt: info.mtime.toISOString() }];
|
|
11476
11779
|
} catch {
|
|
11477
11780
|
return [];
|
|
@@ -11503,7 +11806,7 @@ function createZCodeAdapter() {
|
|
|
11503
11806
|
}
|
|
11504
11807
|
|
|
11505
11808
|
// src/adapters/zed.ts
|
|
11506
|
-
import
|
|
11809
|
+
import os11 from "node:os";
|
|
11507
11810
|
import path22 from "node:path";
|
|
11508
11811
|
function zedThreadsCandidates(home, env) {
|
|
11509
11812
|
const candidates = [];
|
|
@@ -11855,20 +12158,20 @@ async function parseZedSessionFile(dbPath, options) {
|
|
|
11855
12158
|
}
|
|
11856
12159
|
return events.filter((event) => matchesBackfillFilters(event, options));
|
|
11857
12160
|
}
|
|
11858
|
-
async function zedBackfillFiles(sourceRoot, home =
|
|
11859
|
-
const { stat:
|
|
12161
|
+
async function zedBackfillFiles(sourceRoot, home = os11.homedir(), env) {
|
|
12162
|
+
const { stat: stat15 } = await import("node:fs/promises");
|
|
11860
12163
|
if (sourceRoot) {
|
|
11861
12164
|
if (!sourceRoot.endsWith(".db")) {
|
|
11862
12165
|
return [];
|
|
11863
12166
|
}
|
|
11864
|
-
const info = await
|
|
12167
|
+
const info = await stat15(sourceRoot).catch(() => null);
|
|
11865
12168
|
if (!info) {
|
|
11866
12169
|
return [];
|
|
11867
12170
|
}
|
|
11868
12171
|
return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
|
|
11869
12172
|
}
|
|
11870
12173
|
for (const candidatePath of zedThreadsCandidates(home, env)) {
|
|
11871
|
-
const info = await
|
|
12174
|
+
const info = await stat15(candidatePath).catch(() => null);
|
|
11872
12175
|
if (info) {
|
|
11873
12176
|
return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
|
|
11874
12177
|
}
|
|
@@ -12260,11 +12563,26 @@ async function installEntry(entry, options) {
|
|
|
12260
12563
|
await mergeHooksJson(entry.path, entry.content, options);
|
|
12261
12564
|
return;
|
|
12262
12565
|
}
|
|
12566
|
+
if (entry.kind === "hooks-toml" && typeof entry.content === "object") {
|
|
12567
|
+
await mergeHooksToml(entry.path, entry.content, options);
|
|
12568
|
+
return;
|
|
12569
|
+
}
|
|
12263
12570
|
await writeGeneratedFile(entry.path, String(entry.content), {
|
|
12264
12571
|
...options,
|
|
12265
12572
|
onWrite: options.onWrite
|
|
12266
12573
|
});
|
|
12267
12574
|
}
|
|
12575
|
+
async function uninstallEntry(entry, options) {
|
|
12576
|
+
if (entry.kind === "hooks-toml" && typeof entry.content === "object") {
|
|
12577
|
+
await uninstallHooksToml(entry.path, entry.content, options);
|
|
12578
|
+
return;
|
|
12579
|
+
}
|
|
12580
|
+
if (entry.kind === "hooks-json" && typeof entry.content === "object") {
|
|
12581
|
+
await uninstallHooksJson(entry.path, entry.content, options);
|
|
12582
|
+
return;
|
|
12583
|
+
}
|
|
12584
|
+
await uninstallGeneratedFile(entry.path, options);
|
|
12585
|
+
}
|
|
12268
12586
|
async function mergeHooksJson(filePath, content, { dryRun, force, onWrite }) {
|
|
12269
12587
|
const { mkdir: mkdir6, writeFile: writeFile5 } = await import("node:fs/promises");
|
|
12270
12588
|
const pathMod = await import("node:path");
|
|
@@ -12361,6 +12679,218 @@ function hookCommandFromGroup(group) {
|
|
|
12361
12679
|
const hook = group.hooks[0];
|
|
12362
12680
|
return isPlainObject(hook) && typeof hook.command === "string" ? hook.command : void 0;
|
|
12363
12681
|
}
|
|
12682
|
+
async function mergeHooksToml(filePath, content, { dryRun, force, onWrite }) {
|
|
12683
|
+
const { mkdir: mkdir6, writeFile: writeFile5 } = await import("node:fs/promises");
|
|
12684
|
+
const pathMod = await import("node:path");
|
|
12685
|
+
if (dryRun) {
|
|
12686
|
+
onWrite(`Would merge ${filePath}`);
|
|
12687
|
+
return;
|
|
12688
|
+
}
|
|
12689
|
+
const existingText = await readTextIfExists(filePath);
|
|
12690
|
+
if (existingText !== null && existingText.trim() !== "" && !looksLikeToml(existingText) && !force) {
|
|
12691
|
+
throw new Error(
|
|
12692
|
+
`Refusing to update non-TOML file: ${filePath}. Re-run with --force if this is intentional.`
|
|
12693
|
+
);
|
|
12694
|
+
}
|
|
12695
|
+
const desired = Array.isArray(content.hooks) ? content.hooks : [];
|
|
12696
|
+
const nextText = mergeTomlHookRules(existingText ?? "", desired);
|
|
12697
|
+
if (existingText === nextText || existingText === null && nextText === "") {
|
|
12698
|
+
onWrite(`Already installed ${filePath}`);
|
|
12699
|
+
return;
|
|
12700
|
+
}
|
|
12701
|
+
if (existingText !== null) {
|
|
12702
|
+
const existingKeys = new Set(
|
|
12703
|
+
parseTomlHookRules(existingText).map((rule) => `${rule.event}\0${rule.command}`)
|
|
12704
|
+
);
|
|
12705
|
+
const allPresent = desired.every((rule) => existingKeys.has(`${rule.event}\0${rule.command}`));
|
|
12706
|
+
if (allPresent && desired.length > 0) {
|
|
12707
|
+
onWrite(`Already installed ${filePath}`);
|
|
12708
|
+
return;
|
|
12709
|
+
}
|
|
12710
|
+
}
|
|
12711
|
+
await mkdir6(pathMod.dirname(filePath), { recursive: true });
|
|
12712
|
+
await writeFile5(filePath, nextText, "utf8");
|
|
12713
|
+
onWrite(`Installed ${filePath}`);
|
|
12714
|
+
}
|
|
12715
|
+
async function uninstallHooksToml(filePath, content, { dryRun, onWrite }) {
|
|
12716
|
+
const existingText = await readTextIfExists(filePath);
|
|
12717
|
+
if (existingText === null) {
|
|
12718
|
+
onWrite(`Already uninstalled ${filePath}`);
|
|
12719
|
+
return;
|
|
12720
|
+
}
|
|
12721
|
+
const commands = Array.from(new Set(
|
|
12722
|
+
(Array.isArray(content.hooks) ? content.hooks : []).map((rule) => rule.command).filter((cmd) => typeof cmd === "string" && cmd.length > 0)
|
|
12723
|
+
));
|
|
12724
|
+
if (commands.length === 0) {
|
|
12725
|
+
onWrite(`Already uninstalled ${filePath}`);
|
|
12726
|
+
return;
|
|
12727
|
+
}
|
|
12728
|
+
const nextText = removeTomlHookRulesByCommand(existingText, commands);
|
|
12729
|
+
if (nextText === existingText) {
|
|
12730
|
+
onWrite(`Already uninstalled ${filePath}`);
|
|
12731
|
+
return;
|
|
12732
|
+
}
|
|
12733
|
+
if (dryRun) {
|
|
12734
|
+
onWrite(`Would uninstall ${filePath}`);
|
|
12735
|
+
return;
|
|
12736
|
+
}
|
|
12737
|
+
const { writeFile: writeFile5 } = await import("node:fs/promises");
|
|
12738
|
+
await writeFile5(filePath, nextText, "utf8");
|
|
12739
|
+
onWrite(`Uninstalled ${filePath}`);
|
|
12740
|
+
}
|
|
12741
|
+
function collectHookCommandsFromJsonContent(content) {
|
|
12742
|
+
const commands = /* @__PURE__ */ new Set();
|
|
12743
|
+
const walkGroups = (groups) => {
|
|
12744
|
+
if (!Array.isArray(groups)) {
|
|
12745
|
+
return;
|
|
12746
|
+
}
|
|
12747
|
+
for (const group of groups) {
|
|
12748
|
+
if (!isPlainObject(group) || !Array.isArray(group.hooks)) {
|
|
12749
|
+
continue;
|
|
12750
|
+
}
|
|
12751
|
+
for (const hook of group.hooks) {
|
|
12752
|
+
if (isPlainObject(hook) && typeof hook.command === "string" && hook.command) {
|
|
12753
|
+
commands.add(hook.command);
|
|
12754
|
+
}
|
|
12755
|
+
}
|
|
12756
|
+
}
|
|
12757
|
+
};
|
|
12758
|
+
if (isPlainObject(content.hooks)) {
|
|
12759
|
+
for (const groups of Object.values(content.hooks)) {
|
|
12760
|
+
walkGroups(groups);
|
|
12761
|
+
}
|
|
12762
|
+
}
|
|
12763
|
+
for (const [key, value] of Object.entries(content)) {
|
|
12764
|
+
if (key === "hooks" || key === "enable_json_hooks" || !isPlainObject(value)) {
|
|
12765
|
+
continue;
|
|
12766
|
+
}
|
|
12767
|
+
for (const groups of Object.values(value)) {
|
|
12768
|
+
walkGroups(groups);
|
|
12769
|
+
}
|
|
12770
|
+
}
|
|
12771
|
+
return [...commands];
|
|
12772
|
+
}
|
|
12773
|
+
function stripHookCommandsFromGroups(groups, commands) {
|
|
12774
|
+
if (!Array.isArray(groups)) {
|
|
12775
|
+
return groups;
|
|
12776
|
+
}
|
|
12777
|
+
return groups.map((group) => {
|
|
12778
|
+
if (!isPlainObject(group) || !Array.isArray(group.hooks)) {
|
|
12779
|
+
return group;
|
|
12780
|
+
}
|
|
12781
|
+
const nextHooks = group.hooks.filter(
|
|
12782
|
+
(hook) => !(isPlainObject(hook) && typeof hook.command === "string" && commands.has(hook.command))
|
|
12783
|
+
);
|
|
12784
|
+
if (nextHooks.length === 0) {
|
|
12785
|
+
return null;
|
|
12786
|
+
}
|
|
12787
|
+
return { ...group, hooks: nextHooks };
|
|
12788
|
+
}).filter(Boolean);
|
|
12789
|
+
}
|
|
12790
|
+
async function uninstallHooksJson(filePath, content, { dryRun, onWrite }) {
|
|
12791
|
+
const existingText = await readTextIfExists(filePath);
|
|
12792
|
+
if (existingText === null) {
|
|
12793
|
+
onWrite(`Already uninstalled ${filePath}`);
|
|
12794
|
+
return;
|
|
12795
|
+
}
|
|
12796
|
+
let existing;
|
|
12797
|
+
try {
|
|
12798
|
+
existing = JSON.parse(existingText);
|
|
12799
|
+
} catch {
|
|
12800
|
+
onWrite(`Skipped non-JSON file ${filePath}`);
|
|
12801
|
+
return;
|
|
12802
|
+
}
|
|
12803
|
+
if (!isPlainObject(existing)) {
|
|
12804
|
+
onWrite(`Skipped non-object JSON file ${filePath}`);
|
|
12805
|
+
return;
|
|
12806
|
+
}
|
|
12807
|
+
const commands = new Set(collectHookCommandsFromJsonContent(content));
|
|
12808
|
+
if (commands.size === 0) {
|
|
12809
|
+
onWrite(`Already uninstalled ${filePath}`);
|
|
12810
|
+
return;
|
|
12811
|
+
}
|
|
12812
|
+
const next = structuredClone(existing);
|
|
12813
|
+
let changed = false;
|
|
12814
|
+
if (isPlainObject(next.hooks)) {
|
|
12815
|
+
for (const [event, groups] of Object.entries(next.hooks)) {
|
|
12816
|
+
const stripped = stripHookCommandsFromGroups(groups, commands);
|
|
12817
|
+
if (JSON.stringify(stripped) !== JSON.stringify(groups)) {
|
|
12818
|
+
changed = true;
|
|
12819
|
+
if (Array.isArray(stripped) && stripped.length === 0) {
|
|
12820
|
+
delete next.hooks[event];
|
|
12821
|
+
} else {
|
|
12822
|
+
next.hooks[event] = stripped;
|
|
12823
|
+
}
|
|
12824
|
+
}
|
|
12825
|
+
}
|
|
12826
|
+
if (isPlainObject(next.hooks) && Object.keys(next.hooks).length === 0) {
|
|
12827
|
+
delete next.hooks;
|
|
12828
|
+
changed = true;
|
|
12829
|
+
}
|
|
12830
|
+
}
|
|
12831
|
+
for (const [key, value] of Object.entries(next)) {
|
|
12832
|
+
if (key === "hooks" || key === "enable_json_hooks" || !isPlainObject(value)) {
|
|
12833
|
+
continue;
|
|
12834
|
+
}
|
|
12835
|
+
if (!Object.prototype.hasOwnProperty.call(content, key)) {
|
|
12836
|
+
continue;
|
|
12837
|
+
}
|
|
12838
|
+
for (const [event, groups] of Object.entries(value)) {
|
|
12839
|
+
const stripped = stripHookCommandsFromGroups(groups, commands);
|
|
12840
|
+
if (JSON.stringify(stripped) !== JSON.stringify(groups)) {
|
|
12841
|
+
changed = true;
|
|
12842
|
+
if (Array.isArray(stripped) && stripped.length === 0) {
|
|
12843
|
+
delete value[event];
|
|
12844
|
+
} else {
|
|
12845
|
+
value[event] = stripped;
|
|
12846
|
+
}
|
|
12847
|
+
}
|
|
12848
|
+
}
|
|
12849
|
+
if (Object.keys(value).length === 0) {
|
|
12850
|
+
delete next[key];
|
|
12851
|
+
changed = true;
|
|
12852
|
+
}
|
|
12853
|
+
}
|
|
12854
|
+
if (Object.prototype.hasOwnProperty.call(content, "enable_json_hooks") && next.enable_json_hooks === true) {
|
|
12855
|
+
const stillHasNamedHooks = Object.entries(next).some(
|
|
12856
|
+
([key, value]) => key !== "hooks" && key !== "enable_json_hooks" && isPlainObject(value) && Object.values(value).some((groups) => Array.isArray(groups) && groups.length > 0)
|
|
12857
|
+
);
|
|
12858
|
+
if (!stillHasNamedHooks) {
|
|
12859
|
+
delete next.enable_json_hooks;
|
|
12860
|
+
changed = true;
|
|
12861
|
+
}
|
|
12862
|
+
}
|
|
12863
|
+
if (!changed) {
|
|
12864
|
+
onWrite(`Already uninstalled ${filePath}`);
|
|
12865
|
+
return;
|
|
12866
|
+
}
|
|
12867
|
+
if (dryRun) {
|
|
12868
|
+
onWrite(`Would uninstall ${filePath}`);
|
|
12869
|
+
return;
|
|
12870
|
+
}
|
|
12871
|
+
const { writeFile: writeFile5 } = await import("node:fs/promises");
|
|
12872
|
+
await writeFile5(filePath, `${JSON.stringify(next, null, 2)}
|
|
12873
|
+
`, "utf8");
|
|
12874
|
+
onWrite(`Uninstalled ${filePath}`);
|
|
12875
|
+
}
|
|
12876
|
+
async function uninstallGeneratedFile(filePath, { dryRun, onWrite }) {
|
|
12877
|
+
const existingText = await readTextIfExists(filePath);
|
|
12878
|
+
if (existingText === null) {
|
|
12879
|
+
onWrite(`Already uninstalled ${filePath}`);
|
|
12880
|
+
return;
|
|
12881
|
+
}
|
|
12882
|
+
if (!existingText.includes(GENERATED_MARKER) && !existingText.includes("Generated by codetime.")) {
|
|
12883
|
+
onWrite(`Skipped non-vibetime file ${filePath}`);
|
|
12884
|
+
return;
|
|
12885
|
+
}
|
|
12886
|
+
if (dryRun) {
|
|
12887
|
+
onWrite(`Would uninstall ${filePath}`);
|
|
12888
|
+
return;
|
|
12889
|
+
}
|
|
12890
|
+
const { unlink } = await import("node:fs/promises");
|
|
12891
|
+
await unlink(filePath);
|
|
12892
|
+
onWrite(`Uninstalled ${filePath}`);
|
|
12893
|
+
}
|
|
12364
12894
|
|
|
12365
12895
|
// src/lib/config.ts
|
|
12366
12896
|
import { randomUUID } from "node:crypto";
|
|
@@ -12421,7 +12951,7 @@ function defaultMachineName() {
|
|
|
12421
12951
|
init_fs();
|
|
12422
12952
|
|
|
12423
12953
|
// src/lib/logger.ts
|
|
12424
|
-
import { appendFile, mkdir as mkdir4, rename, stat as
|
|
12954
|
+
import { appendFile, mkdir as mkdir4, rename, stat as stat13 } from "node:fs/promises";
|
|
12425
12955
|
import { homedir as homedir2 } from "node:os";
|
|
12426
12956
|
import path24 from "node:path";
|
|
12427
12957
|
var MAX_BYTES = 1 * 1024 * 1024;
|
|
@@ -12439,7 +12969,7 @@ function serializeError(error) {
|
|
|
12439
12969
|
}
|
|
12440
12970
|
async function rotateIfNeeded(file) {
|
|
12441
12971
|
try {
|
|
12442
|
-
const info = await
|
|
12972
|
+
const info = await stat13(file);
|
|
12443
12973
|
if (info.size > MAX_BYTES) {
|
|
12444
12974
|
await rename(file, `${file}.1`).catch(() => {
|
|
12445
12975
|
});
|
|
@@ -12753,6 +13283,7 @@ function createCli(ctx, registry) {
|
|
|
12753
13283
|
});
|
|
12754
13284
|
cli.command("detect", "Show supported local targets and install status").action((options) => detectCommand(normalizeOptions(options), ctx, registry).then(() => 0));
|
|
12755
13285
|
cli.command("install", "Install integration files into detected or requested targets").option("--target <targets>", "Target integrations, comma-separated").option("--targets <targets>", "Target integrations, comma-separated").option("--all", "Install all supported integrations").option("--force", "Overwrite existing non-generated files when needed").action((options) => installCommand(normalizeOptions(options), ctx, registry));
|
|
13286
|
+
cli.command("uninstall", "Remove vibetime integration hooks/files from targets").option("--target <targets>", "Target integrations, comma-separated").option("--targets <targets>", "Target integrations, comma-separated").option("--all", "Uninstall all supported integrations").action((options) => uninstallCommand(normalizeOptions(options), ctx, registry));
|
|
12756
13287
|
cli.command("upgrade", "Check for updates and upgrade to the latest version").option("--check", "Only check for updates, do not install").action((options) => upgradeCommand(normalizeOptions(options), ctx, registry));
|
|
12757
13288
|
cli.command("hook", "Read agent hook JSON from stdin and report a throttled event").option("--agent <name>", "Agent name").option("--project <name>", "Project name").option("--min-interval <seconds>", "Minimum seconds between similar hook reports").action((options) => hookCommand(normalizeOptions(options), ctx));
|
|
12758
13289
|
cli.command("sync-local-trigger", "Trigger one background local sync with throttle and locking").option("--min-interval <seconds>", "Minimum seconds between sync triggers").action((options) => syncLocalTriggerCommand(normalizeOptions(options), ctx, registry));
|
|
@@ -12849,6 +13380,44 @@ async function installCommand(options, ctx, registry) {
|
|
|
12849
13380
|
}
|
|
12850
13381
|
return 0;
|
|
12851
13382
|
}
|
|
13383
|
+
async function uninstallCommand(options, ctx, registry) {
|
|
13384
|
+
const home = resolveHome3(options, ctx);
|
|
13385
|
+
const env = ctx.env;
|
|
13386
|
+
const dryRun = Boolean(options["dry-run"]);
|
|
13387
|
+
const allAdapters = registry.all();
|
|
13388
|
+
const requested = requestedTargets(options);
|
|
13389
|
+
const unknown = requested.filter((id) => !allAdapters.some((a) => a.id === id));
|
|
13390
|
+
if (unknown.length > 0) {
|
|
13391
|
+
throw new Error(`Unknown target(s): ${unknown.join(", ")}`);
|
|
13392
|
+
}
|
|
13393
|
+
const installed = [];
|
|
13394
|
+
for (const adapter of allAdapters) {
|
|
13395
|
+
if (await adapter.isInstalled(home, env)) {
|
|
13396
|
+
installed.push(adapter.id);
|
|
13397
|
+
}
|
|
13398
|
+
}
|
|
13399
|
+
const selectedIds = requested.length > 0 ? requested : options.all ? allAdapters.map((a) => a.id) : installed;
|
|
13400
|
+
if (selectedIds.length === 0) {
|
|
13401
|
+
write(ctx.stderr, "No installed vibetime integrations were found. Use --target <id> or --all.\n");
|
|
13402
|
+
return 1;
|
|
13403
|
+
}
|
|
13404
|
+
for (const adapter of allAdapters.filter((a) => selectedIds.includes(a.id))) {
|
|
13405
|
+
const entries = adapter.installEntries(home, env);
|
|
13406
|
+
if (entries.length === 0) {
|
|
13407
|
+
write(ctx.stdout, `Nothing to uninstall for ${adapter.id}
|
|
13408
|
+
`);
|
|
13409
|
+
continue;
|
|
13410
|
+
}
|
|
13411
|
+
for (const entry of entries) {
|
|
13412
|
+
await uninstallEntry(entry, {
|
|
13413
|
+
dryRun,
|
|
13414
|
+
onWrite: (msg) => write(ctx.stdout, `${msg}
|
|
13415
|
+
`)
|
|
13416
|
+
});
|
|
13417
|
+
}
|
|
13418
|
+
}
|
|
13419
|
+
return 0;
|
|
13420
|
+
}
|
|
12852
13421
|
var NPM_PACKAGE = "@yhong91/vibetime";
|
|
12853
13422
|
async function fetchLatestVersion() {
|
|
12854
13423
|
try {
|
|
@@ -13219,11 +13788,14 @@ async function listBackfillSourceFiles(source, options, ctx) {
|
|
|
13219
13788
|
if (source.id === "zed") {
|
|
13220
13789
|
return zedBackfillFiles(stringOption(options["source-root"]), resolveHome3(options, ctx), ctx.env);
|
|
13221
13790
|
}
|
|
13791
|
+
if (source.id === "pi") {
|
|
13792
|
+
return piBackfillFiles(stringOption(options["source-root"]), resolveHome3(options, ctx), ctx.env);
|
|
13793
|
+
}
|
|
13222
13794
|
const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
|
|
13223
13795
|
const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
|
|
13224
13796
|
const files = fileLists.flat().sort().slice(0, numberOption(options.limit) || void 0);
|
|
13225
13797
|
return Promise.all(files.map(async (filePath) => {
|
|
13226
|
-
const info = await
|
|
13798
|
+
const info = await stat14(filePath);
|
|
13227
13799
|
return { path: filePath, modifiedAt: info.mtime.toISOString() };
|
|
13228
13800
|
}));
|
|
13229
13801
|
}
|
|
@@ -13811,7 +14383,7 @@ function syncLocalRunnerEntryArgs(cliPath) {
|
|
|
13811
14383
|
return [path25.resolve(path25.dirname(cliPath), "../bin/vibetime.mjs")];
|
|
13812
14384
|
}
|
|
13813
14385
|
function resolveHome3(options, ctx) {
|
|
13814
|
-
return path25.resolve(stringOption(options.home) || ctx.env.HOME ||
|
|
14386
|
+
return path25.resolve(stringOption(options.home) || ctx.env.HOME || os12.homedir());
|
|
13815
14387
|
}
|
|
13816
14388
|
function requestedTargets(options) {
|
|
13817
14389
|
const value = options.target || options.targets;
|
|
@@ -13982,6 +14554,7 @@ function helpText() {
|
|
|
13982
14554
|
Usage:
|
|
13983
14555
|
vibetime detect [--json] [--home <path>]
|
|
13984
14556
|
vibetime install [--target codex,claude,opencode,pi] [--all] [--dry-run] [--force] [--home <path>]
|
|
14557
|
+
vibetime uninstall [--target codex,claude,opencode,pi] [--all] [--dry-run] [--home <path>]
|
|
13985
14558
|
vibetime upgrade [--check]
|
|
13986
14559
|
vibetime hook --agent <name>
|
|
13987
14560
|
vibetime backfill discover|plan|import|verify --source codex|claude-code|opencode|pi|all --dry-run [--json] [--batch-size <count>] [--force [--purge-all]]
|
|
@@ -13996,6 +14569,7 @@ Setup:
|
|
|
13996
14569
|
Commands:
|
|
13997
14570
|
detect Show supported local targets and install status.
|
|
13998
14571
|
install Install integration files into detected or requested targets.
|
|
14572
|
+
uninstall Remove vibetime hooks/files from detected or requested targets.
|
|
13999
14573
|
upgrade Check for updates and upgrade to the latest version.
|
|
14000
14574
|
hook Read agent hook JSON from stdin and report a throttled event.
|
|
14001
14575
|
backfill Discover local history and create metadata-only import plans.
|
package/package.json
CHANGED