codiedev 0.9.20 → 0.9.22

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.
@@ -36,7 +36,20 @@ export interface CaptureActivitySummary {
36
36
  status: "pass" | "warn" | "fail";
37
37
  detail: string;
38
38
  }
39
- export declare function summarizeCaptureActivity(tailLines: string[], pendingJobCount: number): CaptureActivitySummary;
39
+ /**
40
+ * When `text` (an error message, code, or capture.log detail) indicates a
41
+ * TLS-trust failure from SSL inspection, return the remediation hint;
42
+ * otherwise null.
43
+ */
44
+ export declare function tlsInterceptionHint(text: string, host: string): string | null;
45
+ /**
46
+ * Flatten a thrown fetch/network error to "<message> (<code>)". undici's
47
+ * fetch wraps the real failure in TypeError("fetch failed") with the cause
48
+ * chained on `.cause` — walk to the deepest cause so doctor reports the
49
+ * actual TLS/socket error instead of "fetch failed".
50
+ */
51
+ export declare function describeFetchError(err: unknown): string;
52
+ export declare function summarizeCaptureActivity(tailLines: string[], pendingJobCount: number, backendHost?: string): CaptureActivitySummary;
40
53
  export interface ServerCaptureResponse {
41
54
  found: boolean;
42
55
  latest: {
@@ -11,6 +11,8 @@ exports.assessCaptureReadiness = assessCaptureReadiness;
11
11
  exports.assessWorkspaceRepos = assessWorkspaceRepos;
12
12
  exports.formatRelativeAge = formatRelativeAge;
13
13
  exports.summarizeLastCapture = summarizeLastCapture;
14
+ exports.tlsInterceptionHint = tlsInterceptionHint;
15
+ exports.describeFetchError = describeFetchError;
14
16
  exports.summarizeCaptureActivity = summarizeCaptureActivity;
15
17
  exports.summarizeServerCapture = summarizeServerCapture;
16
18
  function assessCaptureReadiness(cwd, repos, deps) {
@@ -109,15 +111,56 @@ const STRUCTURED_LINE_RE = /^(\S+) (capture-finish|upload|upload-deferred|stderr
109
111
  function withSuffix(detail) {
110
112
  return `${detail} ${LOG_PATH_SUFFIX}`;
111
113
  }
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
+ 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
+ function tlsInterceptionHint(text, host) {
128
+ if (!TLS_INTERCEPTION_RE.test(text))
129
+ return null;
130
+ return (`your network is intercepting TLS (corporate proxy/VPN). Node ignores the OS cert store — ` +
131
+ `set NODE_EXTRA_CA_CERTS to your corporate root CA file, or ask IT to bypass SSL inspection for ${host}`);
132
+ }
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
+ function describeFetchError(err) {
140
+ if (err === null || err === undefined)
141
+ return "unknown error";
142
+ if (typeof err !== "object")
143
+ return String(err);
144
+ let deepest = err;
145
+ while (deepest.cause && typeof deepest.cause === "object") {
146
+ deepest = deepest.cause;
147
+ }
148
+ const message = deepest.message || String(deepest);
149
+ return deepest.code ? `${message} (${deepest.code})` : message;
150
+ }
112
151
  /**
113
152
  * Derive remediation text and status from the last failing upload-ish line.
114
153
  * Inspects the `detail` field of the log line for http= and msg= tokens.
115
154
  */
116
- function failDetail(rawLine, outcome, lineDetail) {
155
+ function failDetail(rawLine, outcome, lineDetail, backendHost) {
117
156
  const d = lineDetail ?? "";
118
157
  const truncated = rawLine.slice(0, 140);
158
+ const tlsHint = tlsInterceptionHint(d, backendHost ?? "the capture host");
119
159
  let hint;
120
- if (/http=40[13]/.test(d)) {
160
+ if (tlsHint) {
161
+ hint = tlsHint;
162
+ }
163
+ else if (/http=40[13]/.test(d)) {
121
164
  hint = `token invalid or expired — run \`codiedev connect\` with a fresh token`;
122
165
  }
123
166
  else if (/http=5/.test(d) || /fetch|network|ECONN|ETIMEDOUT/i.test(d)) {
@@ -131,7 +174,7 @@ function failDetail(rawLine, outcome, lineDetail) {
131
174
  }
132
175
  return withSuffix(`${hint} (${truncated})`);
133
176
  }
134
- function summarizeCaptureActivity(tailLines, pendingJobCount) {
177
+ function summarizeCaptureActivity(tailLines, pendingJobCount, backendHost) {
135
178
  const parsed = [];
136
179
  for (const raw of tailLines) {
137
180
  const m = STRUCTURED_LINE_RE.exec(raw);
@@ -148,7 +191,7 @@ function summarizeCaptureActivity(tailLines, pendingJobCount) {
148
191
  if (last.outcome === "ERROR" || last.outcome === "gave-up") {
149
192
  return {
150
193
  status: "fail",
151
- detail: failDetail(last.raw, last.outcome, last.detail),
194
+ detail: failDetail(last.raw, last.outcome, last.detail, backendHost),
152
195
  };
153
196
  }
154
197
  }
@@ -154,7 +154,17 @@ async function runDoctor(_args) {
154
154
  status: "pass",
155
155
  detail: `connected to ${config.companyName}`,
156
156
  });
157
- // 2. Token still valid + backend reachable (re-validate)
157
+ // 2. Token still valid + backend reachable (re-validate). The detail names
158
+ // the backend host so users (and their IT) can confirm which domain the CLI
159
+ // talks to — the same host capture uploads use, so this check exercises the
160
+ // exact TLS path a corporate proxy would intercept.
161
+ let backendHost = "the capture host";
162
+ try {
163
+ backendHost = new URL(config.backendUrl).host;
164
+ }
165
+ catch {
166
+ // malformed backendUrl in config — keep the generic placeholder
167
+ }
158
168
  try {
159
169
  const res = await fetch(`${config.backendUrl}/api/cli/validateToken`, {
160
170
  method: "POST",
@@ -168,21 +178,27 @@ async function runDoctor(_args) {
168
178
  checks.push({
169
179
  name: "Backend reachable + token valid",
170
180
  status: "pass",
181
+ detail: backendHost,
171
182
  });
172
183
  }
173
184
  else {
174
185
  checks.push({
175
186
  name: "Backend reachable + token valid",
176
187
  status: "fail",
177
- detail: `HTTP ${res.status} — run \`codiedev connect\` with a fresh token`,
188
+ detail: `HTTP ${res.status} from ${backendHost} — run \`codiedev connect\` with a fresh token`,
178
189
  });
179
190
  }
180
191
  }
181
192
  catch (err) {
193
+ // fetch buries the real TLS/socket error in .cause ("fetch failed" alone
194
+ // is undiagnosable); unwrap it and, when it's an SSL-inspection signature,
195
+ // say what actually fixes it.
196
+ const desc = (0, captureReadiness_1.describeFetchError)(err);
197
+ const tlsHint = (0, captureReadiness_1.tlsInterceptionHint)(desc, backendHost);
182
198
  checks.push({
183
199
  name: "Backend reachable + token valid",
184
200
  status: "fail",
185
- detail: err.message,
201
+ detail: tlsHint ? `${desc} — ${tlsHint}` : `${desc} (host: ${backendHost})`,
186
202
  });
187
203
  }
188
204
  // 3. Authenticated endpoint smoke test (inbox query)
@@ -322,7 +338,7 @@ async function runDoctor(_args) {
322
338
  catch {
323
339
  pendingJobCount = 0;
324
340
  }
325
- const captureActivity = (0, captureReadiness_1.summarizeCaptureActivity)(logTail, pendingJobCount);
341
+ const captureActivity = (0, captureReadiness_1.summarizeCaptureActivity)(logTail, pendingJobCount, backendHost);
326
342
  checks.push({
327
343
  name: "Recent capture activity",
328
344
  status: captureActivity.status,
@@ -1 +1,2 @@
1
+ export declare function candidateKeys(key: string): string[];
1
2
  export declare function runPull(args: string[]): Promise<void>;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.candidateKeys = candidateKeys;
36
37
  exports.runPull = runPull;
37
38
  const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
@@ -69,21 +70,45 @@ function parseArgs(args) {
69
70
  }
70
71
  return { key, version, out };
71
72
  }
73
+ // The backend stores skills under extension-bearing keys (e.g. "pr-push.md"),
74
+ // and lookup is exact-match, so a bare "pr-push" 404s. Make pull forgiving: try
75
+ // the key as given, then a ".md" fallback when it has no extension, so the
76
+ // intuitive bare name (and the hook's suggested key) both resolve.
77
+ function candidateKeys(key) {
78
+ if (/\.[a-z0-9]+$/i.test(key))
79
+ return [key];
80
+ return [key, `${key}.md`];
81
+ }
72
82
  async function runPull(args) {
73
83
  const { key, version, out } = parseArgs(args);
74
84
  const config = (0, shared_1.requireConfig)();
85
+ const keys = candidateKeys(key);
75
86
  const start = Date.now();
76
87
  try {
77
- const res = await (0, shared_1.apiRequest)("GET", "/api/cli/pull", {
78
- config,
79
- query: { key, version },
80
- });
88
+ let res;
89
+ for (let i = 0; i < keys.length; i++) {
90
+ try {
91
+ res = await (0, shared_1.apiRequest)("GET", "/api/cli/pull", {
92
+ config,
93
+ query: { key: keys[i], version },
94
+ });
95
+ break;
96
+ }
97
+ catch (err) {
98
+ const e = err;
99
+ // A 404 on a non-final candidate is retryable (fall through to ".md");
100
+ // anything else — or the last candidate — propagates to the handler.
101
+ if (e.status === 404 && i < keys.length - 1)
102
+ continue;
103
+ throw err;
104
+ }
105
+ }
106
+ const a = res.artifact;
81
107
  (0, shared_1.recordAgentEvent)(config, {
82
108
  tool: "codiedev_pull",
83
109
  ok: true,
84
110
  latencyMs: Date.now() - start,
85
111
  });
86
- const a = res.artifact;
87
112
  if (out) {
88
113
  const absolute = path.isAbsolute(out) ? out : path.resolve(process.cwd(), out);
89
114
  fs.writeFileSync(absolute, a.markdown, "utf8");
@@ -108,7 +133,8 @@ async function runPull(args) {
108
133
  error: e.message,
109
134
  });
110
135
  if (e.status === 404) {
111
- console.error(`Not found: ${key}${version ? ` v${version}` : ""}`);
136
+ const also = keys.length > 1 ? ` (also tried ${keys.slice(1).join(", ")})` : "";
137
+ console.error(`Not found: ${key}${version ? ` v${version}` : ""}${also}`);
112
138
  }
113
139
  else {
114
140
  console.error(`Pull failed: ${e.message}`);
package/dist/hook.d.ts CHANGED
@@ -1,2 +1,6 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ export declare function buildDiscoveryContext(skills: Array<{
3
+ key: string;
4
+ title: string;
5
+ score: number;
6
+ }>): string | null;
package/dist/hook.js CHANGED
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  };
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.buildDiscoveryContext = buildDiscoveryContext;
37
38
  const fs = __importStar(require("fs"));
38
39
  const os = __importStar(require("os"));
39
40
  const path = __importStar(require("path"));
@@ -74,6 +75,26 @@ function getJson(url, bearerToken) {
74
75
  req.end();
75
76
  });
76
77
  }
78
+ // Pure builder for the discovery additionalContext. Returns null when there are
79
+ // no matches. The suggested `codiedev pull` command carries the EXACT top key
80
+ // (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
81
+ // paraphrase the key (pr-push) and the extension-sensitive `pull` 404s.
82
+ function buildDiscoveryContext(skills) {
83
+ if (skills.length === 0)
84
+ return null;
85
+ // Don't trust the API's ordering — rank by score so the suggested pull key is
86
+ // always the highest-confidence match.
87
+ const top = [...skills].sort((a, b) => b.score - a.score).slice(0, 3);
88
+ const lines = top
89
+ .map((s) => `- ${s.key} — ${(s.score * 100).toFixed(0)}% match`)
90
+ .join("\n");
91
+ return ("Your team has a CodieDev skill that fits this task — prefer it over improvising:\n" +
92
+ lines +
93
+ "\nPull and follow it now: `codiedev pull " +
94
+ top[0].key +
95
+ "`. It encodes the team's vetted " +
96
+ "approach. Skip it only if it clearly doesn't apply.");
97
+ }
77
98
  // UserPromptSubmit: query skill discovery for the prompt and, if a team skill
78
99
  // clears the confidence threshold, inject it as additionalContext so the agent
79
100
  // can offer to pull+run it. Fully silent on miss/error — never blocks input.
@@ -86,15 +107,8 @@ async function runDiscover(prompt) {
86
107
  try {
87
108
  const res = await getJson(`${config.backendUrl}/api/cli/skill-discovery?q=${encodeURIComponent(prompt)}`, config.token);
88
109
  const skills = res?.skills ?? [];
89
- if (skills.length > 0) {
90
- const lines = skills
91
- .slice(0, 3)
92
- .map((s) => `- ${s.key} — ${(s.score * 100).toFixed(0)}% match`)
93
- .join("\n");
94
- const additionalContext = "Your team has a CodieDev skill that fits this task — prefer it over improvising:\n" +
95
- lines +
96
- "\nPull and follow it now: `codiedev pull <key>`. It encodes the team's vetted " +
97
- "approach. Skip it only if it clearly doesn't apply.";
110
+ const additionalContext = buildDiscoveryContext(skills);
111
+ if (additionalContext) {
98
112
  process.stdout.write(JSON.stringify({
99
113
  hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
100
114
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.20",
3
+ "version": "0.9.22",
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",