codiedev 0.10.0-beta.4 → 0.10.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +7 -1
  2. package/assets/tui-brand-plugin.tsx +14 -2
  3. package/dist/backup/cache.js +1 -5
  4. package/dist/backup/match.js +0 -6
  5. package/dist/backup/run.js +3 -35
  6. package/dist/captureExclusions.js +0 -12
  7. package/dist/captureLog.js +0 -28
  8. package/dist/captureReadiness.js +0 -58
  9. package/dist/captureSignals.js +66 -3
  10. package/dist/cli.js +41 -18
  11. package/dist/codexHookTrust.js +61 -5
  12. package/dist/codexLimitScan.js +2 -109
  13. package/dist/codie.js +27 -40
  14. package/dist/codieBrandInstall.js +0 -18
  15. package/dist/codieLauncher.js +2 -70
  16. package/dist/codieRuntime.js +19 -23
  17. package/dist/codieRuntimeInstall.js +108 -23
  18. package/dist/codieStartup.js +0 -3
  19. package/dist/codieUsage.js +38 -41
  20. package/dist/commands/ask.js +0 -3
  21. package/dist/commands/capture.js +0 -2
  22. package/dist/commands/createTicket.js +0 -10
  23. package/dist/commands/doctor.js +41 -82
  24. package/dist/commands/inbox.js +0 -2
  25. package/dist/commands/library.js +0 -3
  26. package/dist/commands/pack.js +573 -0
  27. package/dist/commands/ping.js +0 -2
  28. package/dist/commands/pull.js +6 -10
  29. package/dist/commands/push.js +2 -12
  30. package/dist/commands/react.js +0 -2
  31. package/dist/commands/reverseTicket.js +0 -33
  32. package/dist/commands/search.js +0 -4
  33. package/dist/commands/send.js +0 -5
  34. package/dist/commands/share.js +0 -4
  35. package/dist/commands/shared.js +0 -35
  36. package/dist/commands/traces.js +280 -0
  37. package/dist/configDir.js +0 -10
  38. package/dist/connect.js +21 -77
  39. package/dist/connectFlow.js +0 -9
  40. package/dist/copilotHooks.js +395 -0
  41. package/dist/copilotInstallation.js +64 -0
  42. package/dist/cursorHooks.js +40 -17
  43. package/dist/cursorLifecycle.js +109 -0
  44. package/dist/detection.js +0 -30
  45. package/dist/harnessDirs.js +73 -0
  46. package/dist/hook.js +193 -316
  47. package/dist/inboxFormat.js +0 -24
  48. package/dist/launchContext.js +150 -0
  49. package/dist/launcher.js +140 -0
  50. package/dist/limitEvent.js +41 -35
  51. package/dist/limitEventReporter.js +1 -1
  52. package/dist/mcp.js +3 -77
  53. package/dist/nativeSteering.js +24 -0
  54. package/dist/packs/codie.js +182 -0
  55. package/dist/packs/harnesses.js +374 -0
  56. package/dist/packs/hookGate.js +256 -0
  57. package/dist/packs/install.js +285 -0
  58. package/dist/packs/manifest.js +126 -0
  59. package/dist/packs/paths.js +81 -0
  60. package/dist/packs/shim.js +214 -0
  61. package/dist/packs/store.js +155 -0
  62. package/dist/packs/sync.js +170 -0
  63. package/dist/prUrl.js +0 -5
  64. package/dist/repoResolver.js +0 -19
  65. package/dist/secret.js +0 -3
  66. package/dist/security.js +0 -20
  67. package/dist/securityDeepScan.js +1 -14
  68. package/dist/securityDoctor.js +0 -2
  69. package/dist/securityFeature.js +0 -1
  70. package/dist/securityInstall.js +6 -13
  71. package/dist/securityInventory.js +23 -13
  72. package/dist/securityPilot.js +0 -2
  73. package/dist/securityPolicy.js +0 -7
  74. package/dist/selfUpdate.js +331 -0
  75. package/dist/skillMarker.js +23 -0
  76. package/dist/subagents.js +135 -0
  77. package/dist/teamSkillContext.js +10 -0
  78. package/dist/transcriptModels.js +6 -8
  79. package/dist/upgradeNudge.js +0 -30
  80. package/dist/utils.js +286 -733
  81. package/dist/version.js +0 -10
  82. package/dist/versionCheck.js +0 -33
  83. package/package.json +7 -6
  84. package/dist/backup/cache.d.ts +0 -14
  85. package/dist/backup/match.d.ts +0 -2
  86. package/dist/backup/run.d.ts +0 -25
  87. package/dist/captureExclusions.d.ts +0 -30
  88. package/dist/captureLog.d.ts +0 -20
  89. package/dist/captureReadiness.d.ts +0 -65
  90. package/dist/captureSignals.d.ts +0 -19
  91. package/dist/cli.d.ts +0 -16
  92. package/dist/codexHookTrust.d.ts +0 -34
  93. package/dist/codexLimitScan.d.ts +0 -64
  94. package/dist/codie.d.ts +0 -2
  95. package/dist/codieBrandInstall.d.ts +0 -11
  96. package/dist/codieLauncher.d.ts +0 -86
  97. package/dist/codieRuntime.d.ts +0 -29
  98. package/dist/codieRuntimeInstall.d.ts +0 -28
  99. package/dist/codieStartup.d.ts +0 -26
  100. package/dist/codieUsage.d.ts +0 -57
  101. package/dist/commands/ask.d.ts +0 -1
  102. package/dist/commands/capture.d.ts +0 -14
  103. package/dist/commands/createTicket.d.ts +0 -27
  104. package/dist/commands/delete.d.ts +0 -1
  105. package/dist/commands/doctor.d.ts +0 -1
  106. package/dist/commands/inbox.d.ts +0 -3
  107. package/dist/commands/library.d.ts +0 -2
  108. package/dist/commands/note.d.ts +0 -1
  109. package/dist/commands/ping.d.ts +0 -1
  110. package/dist/commands/promote.d.ts +0 -1
  111. package/dist/commands/pull.d.ts +0 -2
  112. package/dist/commands/push.d.ts +0 -33
  113. package/dist/commands/react.d.ts +0 -2
  114. package/dist/commands/reverseTicket.d.ts +0 -2
  115. package/dist/commands/search.d.ts +0 -2
  116. package/dist/commands/security.d.ts +0 -1
  117. package/dist/commands/send.d.ts +0 -2
  118. package/dist/commands/share.d.ts +0 -2
  119. package/dist/commands/shared.d.ts +0 -64
  120. package/dist/configDir.d.ts +0 -1
  121. package/dist/connect.d.ts +0 -1
  122. package/dist/connectFlow.d.ts +0 -13
  123. package/dist/cursorHooks.d.ts +0 -23
  124. package/dist/detection.d.ts +0 -30
  125. package/dist/hook.d.ts +0 -6
  126. package/dist/inboxFormat.d.ts +0 -38
  127. package/dist/limitEvent.d.ts +0 -16
  128. package/dist/limitEventReporter.d.ts +0 -21
  129. package/dist/mcp.d.ts +0 -12
  130. package/dist/prUrl.d.ts +0 -9
  131. package/dist/repoResolver.d.ts +0 -17
  132. package/dist/secret.d.ts +0 -1
  133. package/dist/security.d.ts +0 -102
  134. package/dist/securityDeepScan.d.ts +0 -41
  135. package/dist/securityDoctor.d.ts +0 -32
  136. package/dist/securityFeature.d.ts +0 -5
  137. package/dist/securityInstall.d.ts +0 -63
  138. package/dist/securityInventory.d.ts +0 -54
  139. package/dist/securityPilot.d.ts +0 -19
  140. package/dist/securityPolicy.d.ts +0 -33
  141. package/dist/securitySession.d.ts +0 -6
  142. package/dist/transcriptModels.d.ts +0 -18
  143. package/dist/upgradeNudge.d.ts +0 -25
  144. package/dist/utils.d.ts +0 -346
  145. package/dist/version.d.ts +0 -1
  146. package/dist/versionCheck.d.ts +0 -68
package/README.md CHANGED
@@ -5,9 +5,15 @@ Connect Claude Code, Codex, Cursor, or OpenCode to Codiedev for team session cap
5
5
  ## Usage
6
6
 
7
7
  ```bash
8
- npx codiedev connect
8
+ npm install -g codiedev@latest
9
+ codiedev connect
10
+ codiedev doctor
9
11
  ```
10
12
 
13
+ If global npm installs are blocked, use `npx codiedev@latest connect` and
14
+ `npx codiedev@latest doctor` instead. The agent instructions include the same
15
+ fallback for other commands.
16
+
11
17
  The CLI will prompt you for an API token (get one at https://codiedev.com/portal/integrations/claude-code), then install a capture hook into whichever agent CLIs are present on your machine:
12
18
 
13
19
  - **Claude Code** — `SessionEnd` hook in `~/.claude/settings.json`
@@ -31,6 +31,7 @@ const MARK = [
31
31
  const REFRESH_MS = 30_000
32
32
 
33
33
  const [usageLine, setUsageLine] = createSignal("")
34
+ const [usageTone, setUsageTone] = createSignal("normal")
34
35
 
35
36
  async function startUsagePolling() {
36
37
  let usage: any
@@ -56,7 +57,11 @@ async function startUsagePolling() {
56
57
  })
57
58
  // A failed poll leaves the last good line up rather than blanking it:
58
59
  // a number that stopped updating beats a number that vanished.
59
- if (snapshot) setUsageLine(usage.formatUsageLine(snapshot))
60
+ if (snapshot) {
61
+ setUsageLine(usage.formatUsageLine(snapshot))
62
+ // Older installs of the usage module have no tone; stay muted there.
63
+ if (typeof usage.usageTone === "function") setUsageTone(usage.usageTone(snapshot))
64
+ }
60
65
  } catch {}
61
66
  }
62
67
  void poll()
@@ -69,7 +74,14 @@ void startUsagePolling().catch(() => {})
69
74
 
70
75
  function StatusLine(props: { theme: any }) {
71
76
  return (
72
- <text fg={props.theme.textMuted} selectable={false}>
77
+ <text
78
+ fg={usageTone() === "critical"
79
+ ? (props.theme.error ?? props.theme.textMuted)
80
+ : usageTone() === "warn"
81
+ ? (props.theme.warning ?? props.theme.textMuted)
82
+ : props.theme.textMuted}
83
+ selectable={false}
84
+ >
73
85
  {usageLine()}
74
86
  </text>
75
87
  )
@@ -49,7 +49,7 @@ function loadCache(readFileSync = fs.readFileSync) {
49
49
  return JSON.parse(readFileSync(CACHE_PATH, "utf8"));
50
50
  }
51
51
  catch {
52
- return {}; // missing or corrupt → empty
52
+ return {};
53
53
  }
54
54
  }
55
55
  function saveCache(cache, deps = fs) {
@@ -58,12 +58,8 @@ function saveCache(cache, deps = fs) {
58
58
  deps.writeFileSync(CACHE_PATH, JSON.stringify(cache), { mode: 0o600 });
59
59
  }
60
60
  catch {
61
- // best-effort — a failed cache write just means a redundant push next turn,
62
- // which the server idempotently no-ops.
63
61
  }
64
62
  }
65
- // Pure: given the prior cache, a repo key, and this turn's candidates, return
66
- // the subset whose hash changed plus the cache to persist.
67
63
  function applyCache(prev, repoKey, candidates) {
68
64
  const next = { ...prev };
69
65
  const changed = [];
@@ -1,8 +1,4 @@
1
1
  "use strict";
2
- // Pure path matcher for the auto-backup hook. Decides whether a repo-relative
3
- // path is an "artifact" worth backing up. Deny rules win over allow rules.
4
- // `getContent` is lazy: only called when a path-level allow needs the
5
- // skill-frontmatter exclusion (skills are pushed deliberately, not auto-backed).
6
2
  Object.defineProperty(exports, "__esModule", { value: true });
7
3
  exports.hasSkillFrontmatter = hasSkillFrontmatter;
8
4
  exports.isArtifactPath = isArtifactPath;
@@ -15,7 +11,6 @@ function basename(p) {
15
11
  const i = p.lastIndexOf("/");
16
12
  return i === -1 ? p : p.slice(i + 1);
17
13
  }
18
- // A leading `---` block containing both `name:` and `description:` → skill.
19
14
  function hasSkillFrontmatter(md) {
20
15
  const m = md.match(/^?---\s*\n([\s\S]*?)\n---/);
21
16
  if (!m)
@@ -62,7 +57,6 @@ function isArtifactPath(rel, getContent) {
62
57
  return false;
63
58
  }
64
59
  catch {
65
- // unreadable → treat as non-skill; the push will fail harmlessly later
66
60
  }
67
61
  return true;
68
62
  }
@@ -40,7 +40,6 @@ exports.runBackup = runBackup;
40
40
  exports.runBackupUpload = runBackupUpload;
41
41
  const match_1 = require("./match");
42
42
  const cache_1 = require("./cache");
43
- // Pure: merge two path lists into one, deduped, preserving first-seen order.
44
43
  function unionPaths(a, b) {
45
44
  const seen = new Set();
46
45
  const out = [];
@@ -56,8 +55,6 @@ function unionPaths(a, b) {
56
55
  }
57
56
  return out;
58
57
  }
59
- // Pure: from this turn's git-status paths, keep artifact files whose content
60
- // hash changed since the cache. Returns the changed candidates + next cache.
61
58
  function selectBackupFiles(input) {
62
59
  const candidates = [];
63
60
  for (const rel of input.statusPaths) {
@@ -70,7 +67,7 @@ function selectBackupFiles(input) {
70
67
  c = getContent();
71
68
  }
72
69
  catch {
73
- continue; // deleted/unreadable → skip
70
+ continue;
74
71
  }
75
72
  if (c === undefined || c === null)
76
73
  continue;
@@ -99,11 +96,7 @@ function gitRoot(cwd) {
99
96
  }
100
97
  function gitStatusPaths(root) {
101
98
  try {
102
- // --untracked-files=all is REQUIRED: the default collapses untracked files
103
- // into their top-level directory (a new docs/superpowers/plans/x.md shows as
104
- // just "docs/"), which would make the hook miss brand-new artifact files.
105
99
  const out = (0, child_process_1.execFileSync)("git", ["-C", root, "status", "--porcelain", "-z", "--untracked-files=all"], { encoding: "utf8" });
106
- // -z: NUL-separated, each entry "XY <path>"; rename entries have two paths.
107
100
  return out
108
101
  .split("\0")
109
102
  .filter(Boolean)
@@ -114,8 +107,6 @@ function gitStatusPaths(root) {
114
107
  return [];
115
108
  }
116
109
  }
117
- // All git-tracked files (committed, regardless of dirty state). `git ls-files`
118
- // emits bare NUL-separated paths — no "XY " status prefix — so we don't slice.
119
110
  function gitTrackedPaths(root) {
120
111
  try {
121
112
  const out = (0, child_process_1.execFileSync)("git", ["-C", root, "ls-files", "-z"], {
@@ -127,16 +118,10 @@ function gitTrackedPaths(root) {
127
118
  return [];
128
119
  }
129
120
  }
130
- // The hook's candidate set: dirty/untracked files ∪ all tracked files. Feeding
131
- // tracked files in is what lets a COMMITTED artifact (clean status) still get
132
- // backed up — the hash cache in selectBackupFiles dedups so already-backed-up
133
- // files don't re-upload. selectBackupFiles rejects non-artifact paths cheaply
134
- // (by regex, no content read), so the full tracked list is fine to pass.
135
121
  function gitCandidatePaths(root) {
136
122
  return unionPaths(gitStatusPaths(root), gitTrackedPaths(root));
137
123
  }
138
124
  const sha256 = (s) => crypto.createHash("sha256").update(s, "utf8").digest("hex");
139
- // Stop/sessionEnd hook entry. Best-effort, never throws.
140
125
  function runBackup(cwd, hookScriptPath) {
141
126
  try {
142
127
  if (process.env.CODIEDEV_NO_AUTOBACKUP)
@@ -150,8 +135,6 @@ function runBackup(cwd, hookScriptPath) {
150
135
  if (!root)
151
136
  return;
152
137
  const repoKey = (0, utils_1.getGitRemoteUrl)(root) ?? root;
153
- // Dirty files ∪ tracked files — so committed-but-unbacked artifacts are
154
- // caught too, not just unsaved drafts. Hash cache dedups the rest.
155
138
  const statusPaths = gitCandidatePaths(root);
156
139
  if (statusPaths.length === 0)
157
140
  return;
@@ -164,12 +147,6 @@ function runBackup(cwd, hookScriptPath) {
164
147
  });
165
148
  if (changed.length === 0)
166
149
  return;
167
- // The detached child owns the cache: it writes each file's hash ONLY after a
168
- // successful push, so a genuine push failure (network/500) leaves the hash
169
- // uncached and the file retries next turn. (Persisting here optimistically
170
- // would suppress that retry, since the server no-op only covers successes.)
171
- // Job carries root + repoKey + files; the child re-reads config from disk
172
- // via readConfig() (same machine), avoiding any config-shape guessing.
173
150
  const job = {
174
151
  root,
175
152
  repoKey,
@@ -184,18 +161,13 @@ function runBackup(cwd, hookScriptPath) {
184
161
  child.unref();
185
162
  }
186
163
  catch {
187
- // best-effort
188
164
  }
189
165
  }
190
166
  const DEFAULT_BACKUP_UPLOAD_DEPS = { readConfig: utils_1.readConfig, loadCache: cache_2.loadCache, saveCache: cache_2.saveCache };
191
- // Detached child: push each queued file privately, caching its hash ONLY on a
192
- // successful push (so a failed push retries next turn), then delete the job
193
- // file. Deps are injectable so the cache write is testable without touching the
194
- // global ~/.codiedev cache.
195
167
  async function runBackupUpload(jobFile, deps = DEFAULT_BACKUP_UPLOAD_DEPS) {
196
168
  try {
197
169
  const job = JSON.parse(fs.readFileSync(jobFile, "utf8"));
198
- const config = deps.readConfig(); // same machine — read full config from disk
170
+ const config = deps.readConfig();
199
171
  if (!config)
200
172
  return;
201
173
  if ((0, captureExclusions_1.isCaptureIgnored)(job.root, config.captureIgnoredPaths))
@@ -210,25 +182,21 @@ async function runBackupUpload(jobFile, deps = DEFAULT_BACKUP_UPLOAD_DEPS) {
210
182
  visibility: "private",
211
183
  contentHash: f.hash,
212
184
  });
213
- // Only now that the push landed do we record the hash — a prior failure
214
- // leaves it uncached so the next turn retries it.
215
185
  cache[(0, cache_2.cacheKey)(job.repoKey, f.rel)] = f.hash;
216
186
  dirty = true;
217
187
  }
218
188
  catch {
219
- // one file failing must not abort the rest, and its hash stays uncached
220
189
  }
221
190
  }
222
191
  if (dirty)
223
192
  deps.saveCache(cache);
224
193
  }
225
194
  catch {
226
- // best-effort
227
195
  }
228
196
  finally {
229
197
  try {
230
198
  fs.unlinkSync(jobFile);
231
199
  }
232
- catch { /* gone */ }
200
+ catch { }
233
201
  }
234
202
  }
@@ -47,12 +47,6 @@ const DEFAULT_NORMALIZE_DEPS = {
47
47
  homedir: () => os.homedir(),
48
48
  realpathSync: (value) => fs.realpathSync.native(value),
49
49
  };
50
- /**
51
- * Convert a user-entered path into the canonical absolute form persisted in
52
- * ~/.codiedev/config.json. Existing symlinks resolve to their real target so a
53
- * session cannot bypass an exclusion by entering the same folder through a
54
- * different alias. Non-existent paths still normalize deterministically.
55
- */
56
50
  function normalizeCapturePath(input, deps = {}) {
57
51
  const d = { ...DEFAULT_NORMALIZE_DEPS, ...deps };
58
52
  const trimmed = input.trim();
@@ -94,7 +88,6 @@ function findMatchingCaptureExclusion(candidate, ignoredPaths) {
94
88
  function isCaptureIgnored(candidate, ignoredPaths) {
95
89
  return findMatchingCaptureExclusion(candidate, ignoredPaths) !== null;
96
90
  }
97
- /** Every local root associated with a hook event, deduped in stable order. */
98
91
  function captureScopePaths(input, fallback = process.cwd()) {
99
92
  const values = [input.cwd, ...(input.workspace_roots ?? [])]
100
93
  .filter((value) => typeof value === "string" && value.length > 0);
@@ -103,11 +96,6 @@ function captureScopePaths(input, fallback = process.cwd()) {
103
96
  function shouldIgnoreCaptureInput(config, input, fallback) {
104
97
  return isCaptureIgnored(captureScopePaths(input, fallback), config?.captureIgnoredPaths);
105
98
  }
106
- /**
107
- * Final egress decision for queued transcript uploads. Jobs written by older
108
- * CLI versions have no scope metadata; once any exclusion exists, dropping
109
- * those legacy jobs is safer than guessing that they are public.
110
- */
111
99
  function shouldSkipCaptureUploadJob(job, config) {
112
100
  const ignoredPaths = config?.captureIgnoredPaths ?? [];
113
101
  if (ignoredPaths.length === 0)
@@ -36,47 +36,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CAPTURE_LOG_PATH = exports.CAPTURE_LOG_MAX_BYTES = void 0;
37
37
  exports.appendCaptureLog = appendCaptureLog;
38
38
  exports.readCaptureLogTail = readCaptureLogTail;
39
- // Rotating capture diagnostic log at <codiedevStateDir()>/capture.log
40
- // (~/.codiedev by default; ~/.codiedev-desktop under CODIEDEV_CONFIG_DIR).
41
- //
42
- // Every stage of the capture pipeline (capture-finish child, upload child,
43
- // stderr from detached children) writes one line here so users — and
44
- // `codiedev doctor` — can see exactly why a capture succeeded or failed
45
- // without having to file a support ticket or grep /tmp. The log is
46
- // best-effort throughout: appendCaptureLog NEVER throws into the hook path.
47
39
  const fs = __importStar(require("fs"));
48
40
  const path = __importStar(require("path"));
49
- // Imported from configDir (not utils) — utils.ts imports this module, so
50
- // importing utils.ts here would create a circular import.
51
41
  const configDir_1 = require("./configDir");
52
- // Rotate the log when it exceeds this size (keeps the newest ~half). 256 KB
53
- // holds thousands of entries; a user would have to see hundreds of errors
54
- // before hitting this limit, so it's a generous safety valve.
55
42
  exports.CAPTURE_LOG_MAX_BYTES = 256 * 1024;
56
43
  exports.CAPTURE_LOG_PATH = path.join((0, configDir_1.codiedevStateDir)(), "capture.log");
57
- // Append one structured line to the capture log. Rotates the file when it
58
- // grows past CAPTURE_LOG_MAX_BYTES by truncating it to the newest half (keeps
59
- // the most recent diagnostics). Best-effort: NEVER throws into the hook path —
60
- // a diagnostic log must not break the thing it's diagnosing.
61
44
  function appendCaptureLog(entry, deps = fs, filePath = exports.CAPTURE_LOG_PATH, now = Date.now) {
62
45
  try {
63
46
  const dir = path.dirname(filePath);
64
47
  if (!deps.existsSync(dir))
65
48
  deps.mkdirSync(dir, { recursive: true });
66
49
  if (deps.existsSync(filePath) && deps.statSync(filePath).size >= exports.CAPTURE_LOG_MAX_BYTES) {
67
- // Rotation: keep the newest half. Slice from the middle to a line
68
- // boundary so we never persist a torn (partial) first line.
69
50
  const content = deps.readFileSync(filePath, "utf8");
70
51
  const half = content.slice(Math.floor(content.length / 2));
71
52
  const firstNewline = half.indexOf("\n");
72
- // If no newline is found in the second half, drop it all and start
73
- // fresh — better than keeping a torn line.
74
53
  const fromLine = firstNewline === -1 ? "" : half.slice(firstNewline + 1);
75
54
  deps.writeFileSync(filePath, fromLine);
76
55
  }
77
- // Sanitize before composing: error messages can carry newlines (e.g. a
78
- // proxy's HTML error body in err.message) which would tear the one-line
79
- // format that doctor's parser depends on.
80
56
  const outcome = entry.outcome.replace(/\s+/g, " ").trim();
81
57
  const detail = entry.detail?.replace(/\s+/g, " ").trim();
82
58
  const line = `${new Date(now()).toISOString()} ${entry.stage} session=${entry.sessionId ?? "-"} ${outcome}` +
@@ -85,12 +61,8 @@ function appendCaptureLog(entry, deps = fs, filePath = exports.CAPTURE_LOG_PATH,
85
61
  deps.appendFileSync(filePath, line);
86
62
  }
87
63
  catch {
88
- // diagnostics must never break capture
89
64
  }
90
65
  }
91
- // Read the last N lines from the capture log. Returns [] when the file is
92
- // missing or unreadable — callers (e.g. `codiedev doctor`) treat that as
93
- // "nothing captured yet" rather than an error.
94
66
  function readCaptureLogTail(n, deps = fs, filePath = exports.CAPTURE_LOG_PATH) {
95
67
  try {
96
68
  if (!deps.existsSync(filePath))
@@ -1,11 +1,4 @@
1
1
  "use strict";
2
- // Capture-readiness assessment for `codiedev doctor`. Answers: "from where I'm
3
- // standing right now, would a session actually capture?" — the blind spot that
4
- // let lean-container / submodule / no-remote setups silently produce zero
5
- // sessions while doctor's setup checks stayed green.
6
- //
7
- // Pure + fully injectable so it unit-tests without a real git repo. The default
8
- // production wiring (in doctor.ts) plugs in execSync + the real git helpers.
9
2
  Object.defineProperty(exports, "__esModule", { value: true });
10
3
  exports.assessCaptureReadiness = assessCaptureReadiness;
11
4
  exports.assessWorkspaceRepos = assessWorkspaceRepos;
@@ -79,9 +72,6 @@ function formatRelativeAge(ms) {
79
72
  return `${h}h`;
80
73
  return `${Math.floor(h / 24)}d`;
81
74
  }
82
- // Turns the "last session captured" breadcrumb into a doctor line. This is the
83
- // payoff check: it lets a user confirm their most recent session actually
84
- // landed without opening the portal.
85
75
  function summarizeLastCapture(record, nowMs) {
86
76
  if (!record) {
87
77
  return {
@@ -98,44 +88,18 @@ function summarizeLastCapture(record, nowMs) {
98
88
  }
99
89
  return { status: "pass", detail: `last session captured ${age} ago` };
100
90
  }
101
- // ─── Capture-activity summarizer ──────────────────────────────────────────────
102
- // Turns the tail of ~/.codiedev/capture.log + a count of pending upload jobs
103
- // into a single doctor CheckResult-compatible {status, detail} object.
104
- //
105
- // Pure (no fs/os imports) — all data is injected by the caller (doctor.ts).
106
91
  const LOG_PATH_SUFFIX = "— details: ~/.codiedev/capture.log";
107
- // Parse regex: a valid structured line has exactly this shape.
108
- // stage is one of the four known stages; outcome is a bare token.
109
92
  const STRUCTURED_LINE_RE = /^(\S+) (capture-finish|upload|upload-deferred|stderr) session=(\S+) (\S+)(?: (.*))?$/;
110
- /** Append the shared log-path suffix to any detail string. */
111
93
  function withSuffix(detail) {
112
94
  return `${detail} ${LOG_PATH_SUFFIX}`;
113
95
  }
114
- // TLS-trust failure phrases/codes. These are what Node's TLS stack emits when
115
- // a middlebox (corporate proxy / VPN doing SSL inspection) re-signs HTTPS with
116
- // an internal CA. Node uses its OWN bundled Mozilla trust store — NOT the OS
117
- // store — so installing the corporate cert system-wide fixes curl/git but not
118
- // the CLI; only NODE_EXTRA_CA_CERTS (or an IT bypass) does.
119
- // "certificate has expired" is deliberately absent: that's a server-side cert
120
- // problem, not interception, and the hint would mislead.
121
96
  const TLS_INTERCEPTION_RE = /self.signed certificate|unable to verify the first certificate|unable to get local issuer certificate|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE|UNABLE_TO_GET_ISSUER_CERT_LOCALLY|DEPTH_ZERO_SELF_SIGNED_CERT|CERT_UNTRUSTED/i;
122
- /**
123
- * When `text` (an error message, code, or capture.log detail) indicates a
124
- * TLS-trust failure from SSL inspection, return the remediation hint;
125
- * otherwise null.
126
- */
127
97
  function tlsInterceptionHint(text, host) {
128
98
  if (!TLS_INTERCEPTION_RE.test(text))
129
99
  return null;
130
100
  return (`your network is intercepting TLS (corporate proxy/VPN). Node ignores the OS cert store — ` +
131
101
  `set NODE_EXTRA_CA_CERTS to your corporate root CA file, or ask IT to bypass SSL inspection for ${host}`);
132
102
  }
133
- /**
134
- * Flatten a thrown fetch/network error to "<message> (<code>)". undici's
135
- * fetch wraps the real failure in TypeError("fetch failed") with the cause
136
- * chained on `.cause` — walk to the deepest cause so doctor reports the
137
- * actual TLS/socket error instead of "fetch failed".
138
- */
139
103
  function describeFetchError(err) {
140
104
  if (err === null || err === undefined)
141
105
  return "unknown error";
@@ -148,10 +112,6 @@ function describeFetchError(err) {
148
112
  const message = deepest.message || String(deepest);
149
113
  return deepest.code ? `${message} (${deepest.code})` : message;
150
114
  }
151
- /**
152
- * Derive remediation text and status from the last failing upload-ish line.
153
- * Inspects the `detail` field of the log line for http= and msg= tokens.
154
- */
155
115
  function failDetail(rawLine, outcome, lineDetail, backendHost) {
156
116
  const d = lineDetail ?? "";
157
117
  const truncated = rawLine.slice(0, 140);
@@ -182,10 +142,7 @@ function summarizeCaptureActivity(tailLines, pendingJobCount, backendHost) {
182
142
  continue;
183
143
  parsed.push({ ts: m[1], stage: m[2], session: m[3], outcome: m[4], detail: m[5], raw });
184
144
  }
185
- // We care only about upload-ish lines (upload + upload-deferred).
186
- // capture-finish and stderr lines are noise for this check.
187
145
  const uploadLines = parsed.filter((p) => p.stage === "upload" || p.stage === "upload-deferred");
188
- // Rule 1: last upload-ish outcome is ERROR or gave-up → fail.
189
146
  if (uploadLines.length > 0) {
190
147
  const last = uploadLines[uploadLines.length - 1];
191
148
  if (last.outcome === "ERROR" || last.outcome === "gave-up") {
@@ -195,8 +152,6 @@ function summarizeCaptureActivity(tailLines, pendingJobCount, backendHost) {
195
152
  };
196
153
  }
197
154
  }
198
- // Rule 2: pending jobs queued (last upload wasn't a hard failure, but
199
- // retries are still in-flight).
200
155
  if (pendingJobCount > 0) {
201
156
  const noun = pendingJobCount === 1 ? "job" : "jobs";
202
157
  return {
@@ -204,15 +159,12 @@ function summarizeCaptureActivity(tailLines, pendingJobCount, backendHost) {
204
159
  detail: withSuffix(`${pendingJobCount} pending upload ${noun} queued for retry`),
205
160
  };
206
161
  }
207
- // Rule 3: no upload-ish lines at all — log is empty, garbage-only, or
208
- // only capture-finish lines.
209
162
  if (uploadLines.length === 0) {
210
163
  return {
211
164
  status: "warn",
212
165
  detail: withSuffix("no capture uploads recorded yet on this machine — run a session and exit, then re-run `codiedev doctor`"),
213
166
  };
214
167
  }
215
- // Rule 4a: last upload-ish outcome is ok → pass.
216
168
  const last = uploadLines[uploadLines.length - 1];
217
169
  const hasOk = uploadLines.some((p) => p.outcome === "ok");
218
170
  if (last.outcome === "ok") {
@@ -221,9 +173,6 @@ function summarizeCaptureActivity(tailLines, pendingJobCount, backendHost) {
221
173
  detail: withSuffix(`last upload ok (${last.ts})`),
222
174
  };
223
175
  }
224
- // Rule 4b: last is "deferred" — a throttled/scheduled defer was queued.
225
- // If an ok exists somewhere in the tail → pass (next upload deferred is fine).
226
- // If no ok exists → warn.
227
176
  if (last.outcome === "deferred") {
228
177
  if (hasOk) {
229
178
  return {
@@ -236,7 +185,6 @@ function summarizeCaptureActivity(tailLines, pendingJobCount, backendHost) {
236
185
  detail: withSuffix("deferred uploads were superseded but no completed upload found in the recent log"),
237
186
  };
238
187
  }
239
- // Rule 4c: last is "superseded".
240
188
  if (hasOk) {
241
189
  return {
242
190
  status: "pass",
@@ -248,12 +196,7 @@ function summarizeCaptureActivity(tailLines, pendingJobCount, backendHost) {
248
196
  detail: withSuffix("deferred uploads were superseded but no completed upload found in the recent log"),
249
197
  };
250
198
  }
251
- // Server-confirmed "did my last session land?". Prefers portal truth over the
252
- // local 2xx breadcrumb; the highest-value branch is "uploaded locally but the
253
- // portal has no record" — the exact silent failure devs hit (2xx to nothing,
254
- // wrong identity, backend drop).
255
199
  function summarizeServerCapture(server, local, nowMs) {
256
- // Endpoint unavailable / old backend → fall back to the local breadcrumb.
257
200
  if (server === null)
258
201
  return summarizeLastCapture(local, nowMs);
259
202
  if (server.found && server.latest) {
@@ -266,7 +209,6 @@ function summarizeServerCapture(server, local, nowMs) {
266
209
  }
267
210
  return { status: "pass", detail: `confirmed in the portal — last session captured ${age} ago` };
268
211
  }
269
- // Server reachable but has no session for this user.
270
212
  if (local) {
271
213
  const age = formatRelativeAge(nowMs - local.recordedAt);
272
214
  return {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CAPTURE_SIGNALS_VERSION = void 0;
4
4
  exports.parseCaptureSignals = parseCaptureSignals;
5
+ exports.withLaunch = withLaunch;
5
6
  const transcriptModels_1 = require("./transcriptModels");
6
7
  exports.CAPTURE_SIGNALS_VERSION = 1;
7
8
  const IDLE_MS = 5 * 60_000;
@@ -14,12 +15,23 @@ const skillKey = (value) => typeof value === "string"
14
15
  .trim()
15
16
  .toLowerCase()
16
17
  : "";
17
- /** Compact diagnostic inputs only: never persist prompts, commands or tool output. */
18
18
  function parseCaptureSignals(content, source) {
19
19
  const timestamps = new Set();
20
- const invocations = new Set(), pulls = new Set();
20
+ const invocationCounts = new Map();
21
+ const invocations = new Set(), pulls = new Set(), entrypoints = new Set();
21
22
  const tools = new Map(), calls = new Set();
22
23
  let parsedRecords = 0, truncated = false;
24
+ let premiumRequests, nanoAiu;
25
+ let cursorSubagents;
26
+ const artifacts = new Map();
27
+ let lastInvoked = null;
28
+ const marker = (text) => {
29
+ if (!lastInvoked || artifacts.has(lastInvoked))
30
+ return;
31
+ const m = text.match(/codiedev:artifact=([a-z0-9]{16,64})/i);
32
+ if (m && artifacts.size < LIMIT)
33
+ artifacts.set(lastInvoked, m[1]);
34
+ };
23
35
  const addSkill = (set, value) => {
24
36
  const key = skillKey(value);
25
37
  if (!key || key.length > 200 || !/^[a-z0-9][a-z0-9_:/.-]*$/i.test(key))
@@ -29,6 +41,10 @@ function parseCaptureSignals(content, source) {
29
41
  return;
30
42
  }
31
43
  set.add(key);
44
+ if (set === invocations) {
45
+ invocationCounts.set(key, (invocationCounts.get(key) ?? 0) + 1);
46
+ lastInvoked = key;
47
+ }
32
48
  };
33
49
  const command = (value) => {
34
50
  if (typeof value !== "string")
@@ -66,6 +82,32 @@ function parseCaptureSignals(content, source) {
66
82
  continue;
67
83
  const e = record;
68
84
  parsedRecords++;
85
+ const entrypoint = (value) => {
86
+ if (typeof value === "string" &&
87
+ entrypoints.size < 10 &&
88
+ /^[a-z0-9][a-z0-9_.:-]{0,39}$/i.test(value))
89
+ entrypoints.add(value.toLowerCase());
90
+ };
91
+ entrypoint(e.entrypoint);
92
+ if (e.type === "session_meta" && e.payload) {
93
+ const src = e.payload.source;
94
+ entrypoint(typeof src === "string"
95
+ ? `codex:${src}`
96
+ : src && typeof src === "object"
97
+ ? `codex:${Object.keys(src)[0] ?? "other"}`
98
+ : undefined);
99
+ }
100
+ if (e.type === "cursor.subagent_summary" && Number.isSafeInteger(e.count) && e.count > 0) {
101
+ cursorSubagents = { count: e.count, truncated: e.truncated === true };
102
+ }
103
+ if (e.type === "copilot.skill_invoked")
104
+ addSkill(invocations, e.skill);
105
+ if (e.type === "copilot.usage_summary") {
106
+ if (typeof e.premiumRequests === "number" && e.premiumRequests >= 0)
107
+ premiumRequests = e.premiumRequests;
108
+ if (typeof e.nanoAiu === "number" && e.nanoAiu >= 0)
109
+ nanoAiu = e.nanoAiu;
110
+ }
69
111
  const time = typeof e.timestamp === "string"
70
112
  ? Date.parse(e.timestamp)
71
113
  : e.info?.time?.created;
@@ -76,14 +118,19 @@ function parseCaptureSignals(content, source) {
76
118
  for (const part of blocks) {
77
119
  if (part?.type === "tool_use")
78
120
  tool(part.name, part.id, part.input);
121
+ if (part?.type === "tool_result")
122
+ marker(typeof part.content === "string" ? part.content : JSON.stringify(part.content ?? ""));
79
123
  if (part?.type === "text" && e.type === "user") {
80
124
  for (const match of String(part.text ?? "").matchAll(/<command-name>\/?([a-z0-9:_-]+)<\/command-name>/gi))
81
125
  addSkill(invocations, match[1]);
126
+ marker(String(part.text ?? ""));
82
127
  }
83
128
  }
84
- if (typeof blocks === "string" && e.type === "user")
129
+ if (typeof blocks === "string" && e.type === "user") {
85
130
  for (const match of blocks.matchAll(/<command-name>\/?([a-z0-9:_-]+)<\/command-name>/gi))
86
131
  addSkill(invocations, match[1]);
132
+ marker(blocks);
133
+ }
87
134
  if (e.type === "response_item" &&
88
135
  ["function_call", "custom_tool_call"].includes(e.payload?.type))
89
136
  tool(e.payload.name, e.payload.call_id, e.payload.arguments ?? e.payload.input);
@@ -110,10 +157,26 @@ function parseCaptureSignals(content, source) {
110
157
  activeMs,
111
158
  idleThresholdMs: IDLE_MS,
112
159
  skillInvocations: [...invocations].sort(),
160
+ skillInvocationCounts: [...invocationCounts]
161
+ .map(([skill, count]) => ({ skill, count }))
162
+ .sort((a, b) => a.skill.localeCompare(b.skill)),
163
+ ...(artifacts.size
164
+ ? { skillInvocationArtifacts: [...artifacts].map(([skill, artifactId]) => ({ skill, artifactId })).sort((a, b) => a.skill.localeCompare(b.skill)) }
165
+ : {}),
113
166
  skillPulls: [...pulls].sort(),
114
167
  toolCalls: [...tools]
115
168
  .map(([name, calls]) => ({ name, calls }))
116
169
  .sort((a, b) => a.name.localeCompare(b.name)),
170
+ ...(cursorSubagents ? { subagents: cursorSubagents } : {}),
171
+ ...(entrypoints.size ? { entrypoints: [...entrypoints].sort() } : {}),
172
+ ...(premiumRequests !== undefined ? { premiumRequests } : {}),
173
+ ...(nanoAiu !== undefined ? { nanoAiu } : {}),
117
174
  truncated,
118
175
  };
119
176
  }
177
+ function withLaunch(signals, launch) {
178
+ if (!launch || !/^[a-z0-9][a-z0-9_.:-]{0,39}$/.test(launch))
179
+ return signals;
180
+ const all = new Set([...(signals.entrypoints ?? []), launch]);
181
+ return all.size > 10 ? signals : { ...signals, entrypoints: [...all].sort() };
182
+ }