codiedev 0.9.14 → 0.9.16-beta.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.
- package/dist/hook.js +69 -1
- package/dist/limitEvent.d.ts +8 -0
- package/dist/limitEvent.js +51 -0
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +55 -2
- package/package.json +1 -1
package/dist/hook.js
CHANGED
|
@@ -41,6 +41,7 @@ const https = __importStar(require("https"));
|
|
|
41
41
|
const http = __importStar(require("http"));
|
|
42
42
|
const child_process_1 = require("child_process");
|
|
43
43
|
const utils_1 = require("./utils");
|
|
44
|
+
const limitEvent_1 = require("./limitEvent");
|
|
44
45
|
const version_1 = require("./version");
|
|
45
46
|
function getJson(url, bearerToken) {
|
|
46
47
|
return new Promise((resolve, reject) => {
|
|
@@ -103,6 +104,61 @@ async function runDiscover(prompt) {
|
|
|
103
104
|
}
|
|
104
105
|
process.exit(0);
|
|
105
106
|
}
|
|
107
|
+
// StopFailure (matcher rate_limit): the dev just got blocked. Read the local
|
|
108
|
+
// transcript tail for the ground-truth rate_limit line and POST a tiny event.
|
|
109
|
+
// Best-effort + silent — StopFailure output is ignored by Claude Code anyway.
|
|
110
|
+
async function runStopFailure(hookInput) {
|
|
111
|
+
const config = (0, utils_1.readConfig)();
|
|
112
|
+
if (!config)
|
|
113
|
+
process.exit(0);
|
|
114
|
+
let event = null;
|
|
115
|
+
const tp = hookInput.transcript_path;
|
|
116
|
+
if (tp) {
|
|
117
|
+
try {
|
|
118
|
+
event = (0, limitEvent_1.parseLimitEventFromTranscript)(fs.readFileSync(tp, "utf8"));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* unreadable transcript — fall through to minimal event */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// The hook is registered with matcher "rate_limit", so a miss still means a
|
|
125
|
+
// real limit — emit a minimal event rather than dropping it.
|
|
126
|
+
const payload = event ?? { kind: "other", at: Date.now(), resetText: null };
|
|
127
|
+
try {
|
|
128
|
+
await postJson(`${config.backendUrl}/api/cli/limitEvent`, {
|
|
129
|
+
kind: payload.kind,
|
|
130
|
+
at: payload.at,
|
|
131
|
+
resetText: payload.resetText,
|
|
132
|
+
sessionId: hookInput.session_id,
|
|
133
|
+
}, config.token);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* best-effort */
|
|
137
|
+
}
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
// SessionStart: the capture-coverage denominator. Record that a session opened
|
|
141
|
+
// so the backend can later compute captured ÷ started. Deliberately tiny — no
|
|
142
|
+
// transcript read, no git — so it never slows session startup. Best-effort +
|
|
143
|
+
// silent (SessionStart hook output is ignored by Claude Code).
|
|
144
|
+
async function runSessionStart(hookInput) {
|
|
145
|
+
const config = (0, utils_1.readConfig)();
|
|
146
|
+
if (!config)
|
|
147
|
+
process.exit(0);
|
|
148
|
+
if (hookInput.session_id) {
|
|
149
|
+
try {
|
|
150
|
+
await postJson(`${config.backendUrl}/api/cli/sessionStart`, {
|
|
151
|
+
sessionId: hookInput.session_id,
|
|
152
|
+
startedAt: Date.now(),
|
|
153
|
+
source: "claude-code",
|
|
154
|
+
}, config.token);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* best-effort — never block session startup */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
process.exit(0);
|
|
161
|
+
}
|
|
106
162
|
function postJson(url, body, bearerToken) {
|
|
107
163
|
return new Promise((resolve, reject) => {
|
|
108
164
|
const parsed = new URL(url);
|
|
@@ -229,12 +285,24 @@ async function main() {
|
|
|
229
285
|
await runDiscover(hookInput.prompt);
|
|
230
286
|
return;
|
|
231
287
|
}
|
|
288
|
+
// StopFailure (matcher rate_limit): post the rate-limit event.
|
|
289
|
+
if (process.argv[2] === "limit") {
|
|
290
|
+
await runStopFailure(hookInput);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// SessionStart: post the capture-coverage denominator ping. Tiny + fast.
|
|
294
|
+
if (process.argv[2] === "session-start") {
|
|
295
|
+
await runSessionStart(hookInput);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
232
298
|
// Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets the
|
|
233
|
-
// discovery + auto-backup hooks wired on their next SessionEnd, without
|
|
299
|
+
// discovery + auto-backup + limit hooks wired on their next SessionEnd, without
|
|
234
300
|
// re-connecting. Cheap (settings.json read, writes only when missing), so it
|
|
235
301
|
// stays inline; the costly transcript/git work is what moves to the child.
|
|
236
302
|
(0, utils_1.ensureDiscoveryHookInstalled)();
|
|
237
303
|
(0, utils_1.ensureBackupHookInstalled)();
|
|
304
|
+
(0, utils_1.ensureLimitHookInstalled)();
|
|
305
|
+
(0, utils_1.ensureSessionStartHookInstalled)();
|
|
238
306
|
const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
|
|
239
307
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
240
308
|
? workspace_roots[0]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type LimitKind = "session" | "org_spend" | "other";
|
|
2
|
+
export interface LimitEvent {
|
|
3
|
+
kind: LimitKind;
|
|
4
|
+
at: number;
|
|
5
|
+
resetText: string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function classifyLimitKind(text: string): LimitKind;
|
|
8
|
+
export declare function parseLimitEventFromTranscript(transcript: string): LimitEvent | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyLimitKind = classifyLimitKind;
|
|
4
|
+
exports.parseLimitEventFromTranscript = parseLimitEventFromTranscript;
|
|
5
|
+
function classifyLimitKind(text) {
|
|
6
|
+
const t = text.toLowerCase();
|
|
7
|
+
if (t.includes("session limit"))
|
|
8
|
+
return "session";
|
|
9
|
+
if (t.includes("spend limit") || t.includes("monthly spend"))
|
|
10
|
+
return "org_spend";
|
|
11
|
+
return "other";
|
|
12
|
+
}
|
|
13
|
+
// "… · resets 9:50pm (UTC)" → "9:50pm (UTC)". Null when absent.
|
|
14
|
+
function parseResetText(text) {
|
|
15
|
+
const m = text.match(/resets\s+(.+?)\s*$/i);
|
|
16
|
+
return m ? m[1].trim() : null;
|
|
17
|
+
}
|
|
18
|
+
function messageText(entry) {
|
|
19
|
+
const c = entry?.message?.content;
|
|
20
|
+
if (typeof c === "string")
|
|
21
|
+
return c;
|
|
22
|
+
if (Array.isArray(c))
|
|
23
|
+
return c.map((x) => (x && typeof x === "object" ? x.text ?? "" : "")).join(" ");
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
// Scans transcript JSONL for the LAST top-level `error: "rate_limit"` entry.
|
|
27
|
+
// (Validated shape: error/isApiErrorMessage are top-level, not under message.)
|
|
28
|
+
function parseLimitEventFromTranscript(transcript) {
|
|
29
|
+
let found = null;
|
|
30
|
+
for (const line of transcript.split("\n")) {
|
|
31
|
+
if (!line.includes('"rate_limit"'))
|
|
32
|
+
continue;
|
|
33
|
+
let e;
|
|
34
|
+
try {
|
|
35
|
+
e = JSON.parse(line);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (e?.error !== "rate_limit" && !e?.isApiErrorMessage)
|
|
41
|
+
continue;
|
|
42
|
+
const text = messageText(e);
|
|
43
|
+
const at = e?.timestamp ? Date.parse(e.timestamp) : NaN;
|
|
44
|
+
found = {
|
|
45
|
+
kind: classifyLimitKind(text),
|
|
46
|
+
at: Number.isNaN(at) ? Date.now() : at,
|
|
47
|
+
resetText: parseResetText(text),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return found;
|
|
51
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -73,10 +73,12 @@ export declare function cursorInstalled(): boolean;
|
|
|
73
73
|
* the presence of any `github.copilot*` directory as the Copilot fingerprint.
|
|
74
74
|
*/
|
|
75
75
|
export declare function vscodeCopilotInstalled(): boolean;
|
|
76
|
-
export declare function upsertCodiedevHook(hooks: Record<string, unknown>, eventName: string, subcommand: string): void;
|
|
76
|
+
export declare function upsertCodiedevHook(hooks: Record<string, unknown>, eventName: string, subcommand: string, matcher?: string): void;
|
|
77
77
|
export declare function installHook(): void;
|
|
78
78
|
export declare function ensureDiscoveryHookInstalled(): void;
|
|
79
79
|
export declare function ensureBackupHookInstalled(): void;
|
|
80
|
+
export declare function ensureLimitHookInstalled(): void;
|
|
81
|
+
export declare function ensureSessionStartHookInstalled(): void;
|
|
80
82
|
export declare function installClaudeCodeInstructions(): void;
|
|
81
83
|
export declare function installCodexInstructions(): void;
|
|
82
84
|
export declare function installCursorInstructions(): void;
|
package/dist/utils.js
CHANGED
|
@@ -59,6 +59,8 @@ exports.upsertCodiedevHook = upsertCodiedevHook;
|
|
|
59
59
|
exports.installHook = installHook;
|
|
60
60
|
exports.ensureDiscoveryHookInstalled = ensureDiscoveryHookInstalled;
|
|
61
61
|
exports.ensureBackupHookInstalled = ensureBackupHookInstalled;
|
|
62
|
+
exports.ensureLimitHookInstalled = ensureLimitHookInstalled;
|
|
63
|
+
exports.ensureSessionStartHookInstalled = ensureSessionStartHookInstalled;
|
|
62
64
|
exports.installClaudeCodeInstructions = installClaudeCodeInstructions;
|
|
63
65
|
exports.installCodexInstructions = installCodexInstructions;
|
|
64
66
|
exports.installCursorInstructions = installCursorInstructions;
|
|
@@ -760,7 +762,7 @@ function vscodeCopilotInstalled() {
|
|
|
760
762
|
// with `subcommand`) into the given hook event, stripping any prior codiedev
|
|
761
763
|
// entries first so connect re-runs / upgrades refresh the resolved binary path.
|
|
762
764
|
// Mutates `hooks` in place. Exported for unit testing.
|
|
763
|
-
function upsertCodiedevHook(hooks, eventName, subcommand) {
|
|
765
|
+
function upsertCodiedevHook(hooks, eventName, subcommand, matcher = ".*") {
|
|
764
766
|
if (!hooks[eventName])
|
|
765
767
|
hooks[eventName] = [];
|
|
766
768
|
const existing = hooks[eventName];
|
|
@@ -772,7 +774,7 @@ function upsertCodiedevHook(hooks, eventName, subcommand) {
|
|
|
772
774
|
return !(0, detection_1.isCodiedevHookCommand)(hook.matcher);
|
|
773
775
|
});
|
|
774
776
|
filtered.push({
|
|
775
|
-
matcher
|
|
777
|
+
matcher,
|
|
776
778
|
hooks: [{ type: "command", command: resolveHookCommand(subcommand) }],
|
|
777
779
|
});
|
|
778
780
|
hooks[eventName] = filtered;
|
|
@@ -798,6 +800,11 @@ function installHook() {
|
|
|
798
800
|
upsertCodiedevHook(hooks, "UserPromptSubmit", "discover");
|
|
799
801
|
// Stop → backup (auto-back-up changed artifact files to your private library).
|
|
800
802
|
upsertCodiedevHook(hooks, "Stop", "backup");
|
|
803
|
+
// StopFailure (matcher rate_limit) → limit (post rate-limit event to the team).
|
|
804
|
+
upsertCodiedevHook(hooks, "StopFailure", "limit", "rate_limit");
|
|
805
|
+
// SessionStart → session-start (capture-coverage denominator: record that a
|
|
806
|
+
// session opened, independent of whether it later captures).
|
|
807
|
+
upsertCodiedevHook(hooks, "SessionStart", "session-start");
|
|
801
808
|
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
802
809
|
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
803
810
|
}
|
|
@@ -853,6 +860,52 @@ function ensureBackupHookInstalled() {
|
|
|
853
860
|
}
|
|
854
861
|
catch { /* best-effort */ }
|
|
855
862
|
}
|
|
863
|
+
// Self-heal: ensure the StopFailure (matcher rate_limit) → limit hook is
|
|
864
|
+
// present without a full `connect` re-run, mirroring ensureBackupHookInstalled.
|
|
865
|
+
// Called from the capture hook so an upgraded-but-not-reconnected user gets the
|
|
866
|
+
// limit-event hook wired on their next session. Only writes when missing.
|
|
867
|
+
function ensureLimitHookInstalled() {
|
|
868
|
+
try {
|
|
869
|
+
if (!fs.existsSync(CLAUDE_SETTINGS_PATH))
|
|
870
|
+
return;
|
|
871
|
+
const settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8"));
|
|
872
|
+
const hooks = (settings.hooks ?? {});
|
|
873
|
+
const sf = (hooks.StopFailure ?? []);
|
|
874
|
+
const present = sf.some((h) => {
|
|
875
|
+
const inner = h.hooks;
|
|
876
|
+
return Array.isArray(inner) && inner.some((x) => (0, detection_1.isCodiedevHookCommand)(x.command));
|
|
877
|
+
});
|
|
878
|
+
if (present)
|
|
879
|
+
return;
|
|
880
|
+
settings.hooks = hooks;
|
|
881
|
+
upsertCodiedevHook(hooks, "StopFailure", "limit", "rate_limit");
|
|
882
|
+
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf8");
|
|
883
|
+
}
|
|
884
|
+
catch { /* best-effort */ }
|
|
885
|
+
}
|
|
886
|
+
// Self-heal: ensure the SessionStart → session-start hook is present without a
|
|
887
|
+
// full `connect` re-run, mirroring ensureLimitHookInstalled. Called from the
|
|
888
|
+
// capture hook so an upgraded-but-not-reconnected user starts emitting the
|
|
889
|
+
// capture-coverage denominator on their next session. Only writes when missing.
|
|
890
|
+
function ensureSessionStartHookInstalled() {
|
|
891
|
+
try {
|
|
892
|
+
if (!fs.existsSync(CLAUDE_SETTINGS_PATH))
|
|
893
|
+
return;
|
|
894
|
+
const settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8"));
|
|
895
|
+
const hooks = (settings.hooks ?? {});
|
|
896
|
+
const ss = (hooks.SessionStart ?? []);
|
|
897
|
+
const present = ss.some((h) => {
|
|
898
|
+
const inner = h.hooks;
|
|
899
|
+
return Array.isArray(inner) && inner.some((x) => (0, detection_1.isCodiedevHookCommand)(x.command));
|
|
900
|
+
});
|
|
901
|
+
if (present)
|
|
902
|
+
return;
|
|
903
|
+
settings.hooks = hooks;
|
|
904
|
+
upsertCodiedevHook(hooks, "SessionStart", "session-start");
|
|
905
|
+
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf8");
|
|
906
|
+
}
|
|
907
|
+
catch { /* best-effort */ }
|
|
908
|
+
}
|
|
856
909
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
857
910
|
// MCP server installation — writes the codiedev MCP server config into each
|
|
858
911
|
// agent CLI's expected location so "npx codiedev connect" wires it up once.
|
package/package.json
CHANGED