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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.14",
3
+ "version": "0.9.16-beta.0",
4
4
  "description": "Connect Claude Code, Codex, Cursor, or VS Code Copilot to Codiedev for org-wide session capture and artifact collaboration",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",