codiedev 0.9.14 → 0.9.15
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 +41 -1
- package/dist/limitEvent.d.ts +8 -0
- package/dist/limitEvent.js +51 -0
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +28 -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,39 @@ 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
|
+
}
|
|
106
140
|
function postJson(url, body, bearerToken) {
|
|
107
141
|
return new Promise((resolve, reject) => {
|
|
108
142
|
const parsed = new URL(url);
|
|
@@ -229,12 +263,18 @@ async function main() {
|
|
|
229
263
|
await runDiscover(hookInput.prompt);
|
|
230
264
|
return;
|
|
231
265
|
}
|
|
266
|
+
// StopFailure (matcher rate_limit): post the rate-limit event.
|
|
267
|
+
if (process.argv[2] === "limit") {
|
|
268
|
+
await runStopFailure(hookInput);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
232
271
|
// 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
|
|
272
|
+
// discovery + auto-backup + limit hooks wired on their next SessionEnd, without
|
|
234
273
|
// re-connecting. Cheap (settings.json read, writes only when missing), so it
|
|
235
274
|
// stays inline; the costly transcript/git work is what moves to the child.
|
|
236
275
|
(0, utils_1.ensureDiscoveryHookInstalled)();
|
|
237
276
|
(0, utils_1.ensureBackupHookInstalled)();
|
|
277
|
+
(0, utils_1.ensureLimitHookInstalled)();
|
|
238
278
|
const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
|
|
239
279
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
240
280
|
? 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,11 @@ 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;
|
|
80
81
|
export declare function installClaudeCodeInstructions(): void;
|
|
81
82
|
export declare function installCodexInstructions(): void;
|
|
82
83
|
export declare function installCursorInstructions(): void;
|
package/dist/utils.js
CHANGED
|
@@ -59,6 +59,7 @@ exports.upsertCodiedevHook = upsertCodiedevHook;
|
|
|
59
59
|
exports.installHook = installHook;
|
|
60
60
|
exports.ensureDiscoveryHookInstalled = ensureDiscoveryHookInstalled;
|
|
61
61
|
exports.ensureBackupHookInstalled = ensureBackupHookInstalled;
|
|
62
|
+
exports.ensureLimitHookInstalled = ensureLimitHookInstalled;
|
|
62
63
|
exports.installClaudeCodeInstructions = installClaudeCodeInstructions;
|
|
63
64
|
exports.installCodexInstructions = installCodexInstructions;
|
|
64
65
|
exports.installCursorInstructions = installCursorInstructions;
|
|
@@ -760,7 +761,7 @@ function vscodeCopilotInstalled() {
|
|
|
760
761
|
// with `subcommand`) into the given hook event, stripping any prior codiedev
|
|
761
762
|
// entries first so connect re-runs / upgrades refresh the resolved binary path.
|
|
762
763
|
// Mutates `hooks` in place. Exported for unit testing.
|
|
763
|
-
function upsertCodiedevHook(hooks, eventName, subcommand) {
|
|
764
|
+
function upsertCodiedevHook(hooks, eventName, subcommand, matcher = ".*") {
|
|
764
765
|
if (!hooks[eventName])
|
|
765
766
|
hooks[eventName] = [];
|
|
766
767
|
const existing = hooks[eventName];
|
|
@@ -772,7 +773,7 @@ function upsertCodiedevHook(hooks, eventName, subcommand) {
|
|
|
772
773
|
return !(0, detection_1.isCodiedevHookCommand)(hook.matcher);
|
|
773
774
|
});
|
|
774
775
|
filtered.push({
|
|
775
|
-
matcher
|
|
776
|
+
matcher,
|
|
776
777
|
hooks: [{ type: "command", command: resolveHookCommand(subcommand) }],
|
|
777
778
|
});
|
|
778
779
|
hooks[eventName] = filtered;
|
|
@@ -798,6 +799,8 @@ function installHook() {
|
|
|
798
799
|
upsertCodiedevHook(hooks, "UserPromptSubmit", "discover");
|
|
799
800
|
// Stop → backup (auto-back-up changed artifact files to your private library).
|
|
800
801
|
upsertCodiedevHook(hooks, "Stop", "backup");
|
|
802
|
+
// StopFailure (matcher rate_limit) → limit (post rate-limit event to the team).
|
|
803
|
+
upsertCodiedevHook(hooks, "StopFailure", "limit", "rate_limit");
|
|
801
804
|
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
802
805
|
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
803
806
|
}
|
|
@@ -853,6 +856,29 @@ function ensureBackupHookInstalled() {
|
|
|
853
856
|
}
|
|
854
857
|
catch { /* best-effort */ }
|
|
855
858
|
}
|
|
859
|
+
// Self-heal: ensure the StopFailure (matcher rate_limit) → limit hook is
|
|
860
|
+
// present without a full `connect` re-run, mirroring ensureBackupHookInstalled.
|
|
861
|
+
// Called from the capture hook so an upgraded-but-not-reconnected user gets the
|
|
862
|
+
// limit-event hook wired on their next session. Only writes when missing.
|
|
863
|
+
function ensureLimitHookInstalled() {
|
|
864
|
+
try {
|
|
865
|
+
if (!fs.existsSync(CLAUDE_SETTINGS_PATH))
|
|
866
|
+
return;
|
|
867
|
+
const settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8"));
|
|
868
|
+
const hooks = (settings.hooks ?? {});
|
|
869
|
+
const sf = (hooks.StopFailure ?? []);
|
|
870
|
+
const present = sf.some((h) => {
|
|
871
|
+
const inner = h.hooks;
|
|
872
|
+
return Array.isArray(inner) && inner.some((x) => (0, detection_1.isCodiedevHookCommand)(x.command));
|
|
873
|
+
});
|
|
874
|
+
if (present)
|
|
875
|
+
return;
|
|
876
|
+
settings.hooks = hooks;
|
|
877
|
+
upsertCodiedevHook(hooks, "StopFailure", "limit", "rate_limit");
|
|
878
|
+
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf8");
|
|
879
|
+
}
|
|
880
|
+
catch { /* best-effort */ }
|
|
881
|
+
}
|
|
856
882
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
857
883
|
// MCP server installation — writes the codiedev MCP server config into each
|
|
858
884
|
// agent CLI's expected location so "npx codiedev connect" wires it up once.
|
package/package.json
CHANGED