@aixle/insights 0.2.0 → 0.2.1-staging
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/README.md +36 -8
- package/dist/auth/credentials.d.ts +7 -1
- package/dist/auth/credentials.js +71 -14
- package/dist/auth/exchange.d.ts +1 -1
- package/dist/auth/exchange.js +1 -1
- package/dist/auth/flow.d.ts +8 -1
- package/dist/auth/flow.js +27 -5
- package/dist/auth/keycloak.d.ts +1 -1
- package/dist/auth/keycloak.js +20 -1
- package/dist/cli.d.ts +5 -3
- package/dist/cli.js +69 -21
- package/dist/collect-cursor-payloads.d.ts +4 -3
- package/dist/collect-cursor-payloads.js +8 -5
- package/dist/cursor-checkpoints.d.ts +2 -2
- package/dist/cursor-payload-contract.d.ts +5 -5
- package/dist/cursor-payload-contract.js +6 -0
- package/dist/cursor-settings.d.ts +9 -4
- package/dist/cursor-settings.js +80 -10
- package/dist/health.d.ts +3 -1
- package/dist/health.js +13 -1
- package/dist/hooks/cursor-hooks-mapper.d.ts +3 -3
- package/dist/hooks/cursor-hooks-mapper.js +1 -1
- package/dist/hooks/cursor-hooks-reader.d.ts +2 -0
- package/dist/hooks/cursor-hooks-reader.js +2 -2
- package/dist/install/cursor.d.ts +34 -0
- package/dist/install/cursor.js +193 -0
- package/dist/install/index.d.ts +6 -4
- package/dist/install/index.js +6 -1
- package/dist/lib/client.d.ts +7 -0
- package/dist/lib/client.js +17 -0
- package/dist/lib/config.js +7 -2
- package/dist/lib/project-resolver.d.ts +5 -4
- package/dist/lib/project-resolver.js +20 -8
- package/dist/lib/transport-security.d.ts +1 -0
- package/dist/lib/transport-security.js +1 -1
- package/dist/readers/claude.d.ts +54 -6
- package/dist/readers/claude.js +154 -2
- package/dist/readers/cursor.d.ts +10 -7
- package/dist/readers/cursor.js +101 -15
- package/dist/server.d.ts +20 -3
- package/dist/server.js +101 -67
- package/dist/state.js +7 -2
- package/dist/sync.d.ts +4 -2
- package/dist/sync.js +61 -46
- package/package.json +2 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, copyFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { desiredAixleInsightsEntry, aixleInsightsEntryMatchesDesired } from "./claude.js";
|
|
6
|
+
export function defaultCursorUserConfigPath() {
|
|
7
|
+
return join(homedir(), ".cursor", "mcp.json");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Legacy keys to clean up on install: "db90" (pre-rebrand) and "insights" (an
|
|
11
|
+
* earlier local-dev-only convention documented in scripts/reset-local-env.mjs,
|
|
12
|
+
* never the production installer's key — see DB90DV-560 orientation.md).
|
|
13
|
+
*/
|
|
14
|
+
const LEGACY_MCP_KEYS = ["db90", "insights"];
|
|
15
|
+
const AIXLE_INSIGHTS_MCP_KEY = "aixle-insights";
|
|
16
|
+
const BACKUP_SUFFIX = ".aixle-insights-backup";
|
|
17
|
+
function readRootObject(path) {
|
|
18
|
+
if (!existsSync(path)) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(path, "utf-8");
|
|
23
|
+
if (!raw.trim()) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
28
|
+
return {
|
|
29
|
+
kind: "error",
|
|
30
|
+
message: `${path}: top-level JSON value must be an object.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
37
|
+
return { kind: "error", message: `Cannot read ${path}: ${msg}` };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function atomicWriteJson(path, data) {
|
|
41
|
+
try {
|
|
42
|
+
const dir = dirname(path);
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
const serialized = `${JSON.stringify(data, null, 2)}\n`;
|
|
45
|
+
const tmpPath = join(dir, `.aixle-insights-cursor-json-${randomBytes(8).toString("hex")}.tmp`);
|
|
46
|
+
writeFileSync(tmpPath, serialized, "utf-8");
|
|
47
|
+
renameSync(tmpPath, path);
|
|
48
|
+
return { kind: "installed" };
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
52
|
+
return { kind: "error", message: `Failed to write ${path}: ${msg}` };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Copy the current file to a backup path, unless a backup already exists (never clobber the user's original). */
|
|
56
|
+
function backupIfNeeded(path) {
|
|
57
|
+
if (!existsSync(path))
|
|
58
|
+
return;
|
|
59
|
+
const backupPath = path + BACKUP_SUFFIX;
|
|
60
|
+
if (existsSync(backupPath))
|
|
61
|
+
return;
|
|
62
|
+
copyFileSync(path, backupPath);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Merges top-level `mcpServers.aixle-insights` into ~/.cursor/mcp.json (or overridden path).
|
|
66
|
+
* Preserves all other keys and MCP server entries. Backs up the existing file
|
|
67
|
+
* (once) before the first write, and removes legacy "db90"/"insights" keys so
|
|
68
|
+
* Cursor does not spawn duplicate aixle-insights servers.
|
|
69
|
+
*/
|
|
70
|
+
export function installCursorUserMcp(options = {}) {
|
|
71
|
+
const path = options.cursorConfigPath ?? defaultCursorUserConfigPath();
|
|
72
|
+
const force = options.force === true;
|
|
73
|
+
const desired = desiredAixleInsightsEntry();
|
|
74
|
+
const rootRead = readRootObject(path);
|
|
75
|
+
if ("kind" in rootRead && typeof rootRead.kind === "string" && rootRead.kind === "error") {
|
|
76
|
+
return rootRead;
|
|
77
|
+
}
|
|
78
|
+
const root = {
|
|
79
|
+
...rootRead,
|
|
80
|
+
};
|
|
81
|
+
const rawServers = root["mcpServers"];
|
|
82
|
+
let mcpServers;
|
|
83
|
+
if (rawServers === undefined) {
|
|
84
|
+
mcpServers = {};
|
|
85
|
+
}
|
|
86
|
+
else if (typeof rawServers === "object" &&
|
|
87
|
+
rawServers !== null &&
|
|
88
|
+
!Array.isArray(rawServers)) {
|
|
89
|
+
mcpServers = { ...rawServers };
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
return {
|
|
93
|
+
kind: "error",
|
|
94
|
+
message: `Invalid Cursor MCP config at ${path}: "mcpServers" must be an object when present.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const existing = mcpServers[AIXLE_INSIGHTS_MCP_KEY];
|
|
98
|
+
const hasLegacyKey = LEGACY_MCP_KEYS.some((k) => mcpServers[k] !== undefined);
|
|
99
|
+
if (existing !== undefined && aixleInsightsEntryMatchesDesired(existing, desired)) {
|
|
100
|
+
if (!hasLegacyKey) {
|
|
101
|
+
return { kind: "already-configured" };
|
|
102
|
+
}
|
|
103
|
+
backupIfNeeded(path);
|
|
104
|
+
for (const key of LEGACY_MCP_KEYS)
|
|
105
|
+
delete mcpServers[key];
|
|
106
|
+
root["mcpServers"] = mcpServers;
|
|
107
|
+
return atomicWriteJson(path, root);
|
|
108
|
+
}
|
|
109
|
+
if (existing !== undefined && !force) {
|
|
110
|
+
return {
|
|
111
|
+
kind: "requires-force",
|
|
112
|
+
detail: 'A different "aixle-insights" MCP server entry already exists in ~/.cursor/mcp.json. Re-run with `init --force` to replace only that entry.',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
backupIfNeeded(path);
|
|
116
|
+
for (const key of LEGACY_MCP_KEYS)
|
|
117
|
+
delete mcpServers[key];
|
|
118
|
+
mcpServers[AIXLE_INSIGHTS_MCP_KEY] = { command: desired.command, args: desired.args };
|
|
119
|
+
root["mcpServers"] = mcpServers;
|
|
120
|
+
return atomicWriteJson(path, root);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Removes the aixle-insights entry from ~/.cursor/mcp.json without disturbing any
|
|
124
|
+
* other server entry — including ones the user or Cursor added after install.
|
|
125
|
+
* Works on the CURRENT file (never a full-file revert to the backup). If a
|
|
126
|
+
* pre-install backup exists, the user's ORIGINAL aixle-insights entry (if any)
|
|
127
|
+
* is restored from it; otherwise our key is simply removed. The backup file is
|
|
128
|
+
* cleaned up either way.
|
|
129
|
+
*/
|
|
130
|
+
export function uninstallCursorUserMcp(options = {}) {
|
|
131
|
+
const path = options.cursorConfigPath ?? defaultCursorUserConfigPath();
|
|
132
|
+
const backupPath = path + BACKUP_SUFFIX;
|
|
133
|
+
const hasBackup = existsSync(backupPath);
|
|
134
|
+
const cleanupBackup = () => {
|
|
135
|
+
if (hasBackup) {
|
|
136
|
+
try {
|
|
137
|
+
unlinkSync(backupPath);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* non-fatal */
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
if (!existsSync(path)) {
|
|
145
|
+
cleanupBackup();
|
|
146
|
+
return { kind: "noop" };
|
|
147
|
+
}
|
|
148
|
+
const rootRead = readRootObject(path);
|
|
149
|
+
if ("kind" in rootRead && typeof rootRead.kind === "string" && rootRead.kind === "error") {
|
|
150
|
+
return rootRead;
|
|
151
|
+
}
|
|
152
|
+
const root = { ...rootRead };
|
|
153
|
+
const rawServers = root["mcpServers"];
|
|
154
|
+
if (typeof rawServers !== "object" || rawServers === null || Array.isArray(rawServers)) {
|
|
155
|
+
cleanupBackup();
|
|
156
|
+
return { kind: "noop" };
|
|
157
|
+
}
|
|
158
|
+
const mcpServers = { ...rawServers };
|
|
159
|
+
// If a backup exists, recover the user's ORIGINAL aixle-insights entry (if they
|
|
160
|
+
// had one we overwrote) rather than just deleting ours — but only that one key,
|
|
161
|
+
// leaving every current sibling entry intact.
|
|
162
|
+
let restoredPrior = false;
|
|
163
|
+
if (hasBackup) {
|
|
164
|
+
const backupRead = readRootObject(backupPath);
|
|
165
|
+
const backupIsObject = !("kind" in backupRead &&
|
|
166
|
+
typeof backupRead.kind === "string" &&
|
|
167
|
+
backupRead.kind === "error");
|
|
168
|
+
if (backupIsObject) {
|
|
169
|
+
const backupServers = backupRead["mcpServers"];
|
|
170
|
+
if (typeof backupServers === "object" && backupServers !== null && !Array.isArray(backupServers)) {
|
|
171
|
+
const priorEntry = backupServers[AIXLE_INSIGHTS_MCP_KEY];
|
|
172
|
+
if (priorEntry !== undefined) {
|
|
173
|
+
mcpServers[AIXLE_INSIGHTS_MCP_KEY] = priorEntry;
|
|
174
|
+
restoredPrior = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!restoredPrior) {
|
|
180
|
+
if (mcpServers[AIXLE_INSIGHTS_MCP_KEY] === undefined) {
|
|
181
|
+
cleanupBackup();
|
|
182
|
+
return { kind: "noop" };
|
|
183
|
+
}
|
|
184
|
+
delete mcpServers[AIXLE_INSIGHTS_MCP_KEY];
|
|
185
|
+
}
|
|
186
|
+
root["mcpServers"] = mcpServers;
|
|
187
|
+
const writeResult = atomicWriteJson(path, root);
|
|
188
|
+
if (writeResult.kind === "error") {
|
|
189
|
+
return writeResult;
|
|
190
|
+
}
|
|
191
|
+
cleanupBackup();
|
|
192
|
+
return restoredPrior ? { kind: "restored", backupPath } : { kind: "removed" };
|
|
193
|
+
}
|
package/dist/install/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type InstallClaudeUserMcpOptions, type InstallResult } from "./claude.js";
|
|
2
|
-
|
|
3
|
-
export type
|
|
2
|
+
import { type InstallCursorUserMcpOptions, type UninstallResult } from "./cursor.js";
|
|
3
|
+
export type SupportedEditor = "claude" | "cursor";
|
|
4
|
+
export type { InstallClaudeUserMcpOptions, InstallCursorUserMcpOptions, InstallResult, UninstallResult, };
|
|
4
5
|
/**
|
|
5
|
-
* Editor dispatch for MCP install hooks.
|
|
6
|
+
* Editor dispatch for MCP install hooks.
|
|
6
7
|
*/
|
|
7
|
-
export declare function installEditorMcp(editor: SupportedEditor, options?: InstallClaudeUserMcpOptions): InstallResult;
|
|
8
|
+
export declare function installEditorMcp(editor: SupportedEditor, options?: InstallClaudeUserMcpOptions & InstallCursorUserMcpOptions): InstallResult;
|
|
8
9
|
export { installClaudeUserMcp, defaultClaudeUserConfigPath, desiredAixleInsightsEntry, aixleInsightsEntryMatchesDesired, } from "./claude.js";
|
|
10
|
+
export { installCursorUserMcp, uninstallCursorUserMcp, defaultCursorUserConfigPath, } from "./cursor.js";
|
package/dist/install/index.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { installClaudeUserMcp, } from "./claude.js";
|
|
2
|
+
import { installCursorUserMcp, } from "./cursor.js";
|
|
2
3
|
/**
|
|
3
|
-
* Editor dispatch for MCP install hooks.
|
|
4
|
+
* Editor dispatch for MCP install hooks.
|
|
4
5
|
*/
|
|
5
6
|
export function installEditorMcp(editor, options = {}) {
|
|
6
7
|
if (editor === "claude") {
|
|
7
8
|
return installClaudeUserMcp(options);
|
|
8
9
|
}
|
|
10
|
+
if (editor === "cursor") {
|
|
11
|
+
return installCursorUserMcp(options);
|
|
12
|
+
}
|
|
9
13
|
return { kind: "error", message: `Unsupported editor for MCP install: ${String(editor)}` };
|
|
10
14
|
}
|
|
11
15
|
export { installClaudeUserMcp, defaultClaudeUserConfigPath, desiredAixleInsightsEntry, aixleInsightsEntryMatchesDesired, } from "./claude.js";
|
|
16
|
+
export { installCursorUserMcp, uninstallCursorUserMcp, defaultCursorUserConfigPath, } from "./cursor.js";
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ export interface IngestPayload {
|
|
|
8
8
|
[key: string]: unknown;
|
|
9
9
|
}
|
|
10
10
|
export interface PostEventOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Skip the HTTPS-or-loopback gate for this host. Only ever set this to
|
|
13
|
+
* `true` when the caller has independently confirmed the user explicitly
|
|
14
|
+
* consented via `init --insecure` for this exact credential (see
|
|
15
|
+
* `StoredCredentials.insecureHttpAllowed`). Defaults to `false`.
|
|
16
|
+
*/
|
|
17
|
+
allowInsecureHttp?: boolean;
|
|
11
18
|
/** Override default console.error on non-ok HTTP response. */
|
|
12
19
|
onHttpError?: (status: number, statusText: string, body: string) => void;
|
|
13
20
|
/** Override default console.error on network-level failure. */
|
package/dist/lib/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { evaluateTransportSecurity } from "./transport-security.js";
|
|
1
2
|
/**
|
|
2
3
|
* POST a single event payload to the db90 ingest endpoint.
|
|
3
4
|
*
|
|
@@ -10,6 +11,22 @@
|
|
|
10
11
|
* Never throws — callers can rely on Promise.allSettled-style aggregation.
|
|
11
12
|
*/
|
|
12
13
|
export async function postEvent(payload, host, token, options = {}) {
|
|
14
|
+
const transportSecurity = evaluateTransportSecurity(host, {
|
|
15
|
+
allowInsecureHttp: options.allowInsecureHttp === true,
|
|
16
|
+
label: "DB90 ingest host",
|
|
17
|
+
});
|
|
18
|
+
if (!transportSecurity.ok) {
|
|
19
|
+
// Deliberately does NOT call options.onNetworkError/onHttpError: the retry
|
|
20
|
+
// wrapper in src/client.ts treats those as transient and retries with
|
|
21
|
+
// backoff. A scheme rejection is permanent — retrying wastes ~21s per
|
|
22
|
+
// event for nothing. Bare console.error mirrors this file's existing
|
|
23
|
+
// unrecoverable-failure logging style.
|
|
24
|
+
console.error(`Blocked event send — ${transportSecurity.error}`);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (transportSecurity.warning) {
|
|
28
|
+
console.error(`Warning: ${transportSecurity.warning}`);
|
|
29
|
+
}
|
|
13
30
|
const url = `${host.replace(/\/$/, "")}/api/v1/ingest/events`;
|
|
14
31
|
const headers = {
|
|
15
32
|
"Content-Type": "application/json",
|
package/dist/lib/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { mcpLog } from "../log.js";
|
|
3
4
|
/**
|
|
4
5
|
* Load a connector's `config.json` from disk. Returns `{}` on missing or
|
|
5
6
|
* malformed files — callers fall back to env vars / CLI flags / defaults.
|
|
@@ -32,8 +33,12 @@ export function loadBaseConfig(configDir, parsePricing) {
|
|
|
32
33
|
return result;
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
catch {
|
|
36
|
-
|
|
36
|
+
catch (err) {
|
|
37
|
+
const code = err?.code;
|
|
38
|
+
if (code !== "ENOENT") {
|
|
39
|
+
// Config file exists but failed to parse — distinguishes tampering from "never created".
|
|
40
|
+
mcpLog.warn("config_parse_failed", { path: configPath, error: err instanceof Error ? err.message : String(err) }, false);
|
|
41
|
+
}
|
|
37
42
|
}
|
|
38
43
|
return {};
|
|
39
44
|
}
|
|
@@ -6,8 +6,8 @@ export interface LookupResult {
|
|
|
6
6
|
project_id: string;
|
|
7
7
|
name: string;
|
|
8
8
|
}
|
|
9
|
-
export declare function resolveProjectId(flagValue: string | undefined, configValue: string | undefined, host: string, token: string, verbose: boolean): Promise<ProjectResolution>;
|
|
10
|
-
export declare function resolveProjectIdForRepoPath(repoPath: string, host: string, token: string, verbose: boolean): Promise<ProjectResolution>;
|
|
9
|
+
export declare function resolveProjectId(flagValue: string | undefined, configValue: string | undefined, host: string, token: string, verbose: boolean, allowInsecureHttp?: boolean): Promise<ProjectResolution>;
|
|
10
|
+
export declare function resolveProjectIdForRepoPath(repoPath: string, host: string, token: string, verbose: boolean, allowInsecureHttp?: boolean): Promise<ProjectResolution>;
|
|
11
11
|
export declare function getGitRemote(verbose: boolean): string | null;
|
|
12
12
|
export declare function getGitRemoteForPath(repoPath: string, verbose: boolean): string | null;
|
|
13
13
|
/**
|
|
@@ -24,7 +24,7 @@ export declare function canonicalizeGitRemote(remote: string, verbose: boolean):
|
|
|
24
24
|
* git remote. Expand to remotes the API lookup understands (`Project.normalize_git_remote`).
|
|
25
25
|
*/
|
|
26
26
|
export declare function repoNameToGitRemoteCandidates(repoName: string): string[];
|
|
27
|
-
export declare function lookupProjectByRepoName(repoName: string, host: string, token: string, verbose: boolean): Promise<LookupResult | "not-found" | null>;
|
|
27
|
+
export declare function lookupProjectByRepoName(repoName: string, host: string, token: string, verbose: boolean, allowInsecureHttp?: boolean): Promise<LookupResult | "not-found" | null>;
|
|
28
28
|
/** Payload shape shared by db90-cursor and telemetry-mcp commit mappers. */
|
|
29
29
|
export interface CommitAttributionPayload {
|
|
30
30
|
event_type?: string;
|
|
@@ -44,5 +44,6 @@ export declare function enrichCommitProjectAttribution(payloads: CommitAttributi
|
|
|
44
44
|
host: string;
|
|
45
45
|
token: string;
|
|
46
46
|
verbose?: boolean;
|
|
47
|
+
allowInsecureHttp?: boolean;
|
|
47
48
|
}): Promise<void>;
|
|
48
|
-
export declare function lookupProjectByRemote(gitRemote: string, host: string, token: string, verbose: boolean): Promise<LookupResult | "not-found" | null>;
|
|
49
|
+
export declare function lookupProjectByRemote(gitRemote: string, host: string, token: string, verbose: boolean, allowInsecureHttp?: boolean): Promise<LookupResult | "not-found" | null>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { evaluateTransportSecurity } from "./transport-security.js";
|
|
2
3
|
/** Coerce empty string to undefined so "" is treated as "not set" */
|
|
3
4
|
function coerce(val) {
|
|
4
5
|
return val === "" ? undefined : val;
|
|
5
6
|
}
|
|
6
|
-
export async function resolveProjectId(flagValue, configValue, host, token, verbose) {
|
|
7
|
+
export async function resolveProjectId(flagValue, configValue, host, token, verbose, allowInsecureHttp = false) {
|
|
7
8
|
const flag = coerce(flagValue);
|
|
8
9
|
const config = coerce(configValue);
|
|
9
10
|
if (flag !== undefined)
|
|
@@ -13,18 +14,18 @@ export async function resolveProjectId(flagValue, configValue, host, token, verb
|
|
|
13
14
|
const gitRemote = getGitRemote(verbose);
|
|
14
15
|
if (gitRemote === null)
|
|
15
16
|
return { projectId: null, source: "none" };
|
|
16
|
-
const result = await lookupProjectByRemote(gitRemote, host, token, verbose);
|
|
17
|
+
const result = await lookupProjectByRemote(gitRemote, host, token, verbose, allowInsecureHttp);
|
|
17
18
|
if (result === "not-found")
|
|
18
19
|
return { projectId: null, source: "auto-detect-not-found" };
|
|
19
20
|
if (result !== null)
|
|
20
21
|
return { projectId: result.project_id, source: "auto-detect" };
|
|
21
22
|
return { projectId: null, source: "none" };
|
|
22
23
|
}
|
|
23
|
-
export async function resolveProjectIdForRepoPath(repoPath, host, token, verbose) {
|
|
24
|
+
export async function resolveProjectIdForRepoPath(repoPath, host, token, verbose, allowInsecureHttp = false) {
|
|
24
25
|
const gitRemote = getGitRemoteForPath(repoPath, verbose);
|
|
25
26
|
if (gitRemote === null)
|
|
26
27
|
return { projectId: null, source: "none" };
|
|
27
|
-
const result = await lookupProjectByRemote(gitRemote, host, token, verbose);
|
|
28
|
+
const result = await lookupProjectByRemote(gitRemote, host, token, verbose, allowInsecureHttp);
|
|
28
29
|
if (result === "not-found")
|
|
29
30
|
return { projectId: null, source: "auto-detect-not-found" };
|
|
30
31
|
if (result !== null)
|
|
@@ -133,7 +134,7 @@ export function repoNameToGitRemoteCandidates(repoName) {
|
|
|
133
134
|
}
|
|
134
135
|
return [];
|
|
135
136
|
}
|
|
136
|
-
export async function lookupProjectByRepoName(repoName, host, token, verbose) {
|
|
137
|
+
export async function lookupProjectByRepoName(repoName, host, token, verbose, allowInsecureHttp = false) {
|
|
137
138
|
const candidates = repoNameToGitRemoteCandidates(repoName);
|
|
138
139
|
if (candidates.length === 0) {
|
|
139
140
|
if (verbose)
|
|
@@ -141,7 +142,7 @@ export async function lookupProjectByRepoName(repoName, host, token, verbose) {
|
|
|
141
142
|
return "not-found";
|
|
142
143
|
}
|
|
143
144
|
for (const candidate of candidates) {
|
|
144
|
-
const result = await lookupProjectByRemote(candidate, host, token, verbose);
|
|
145
|
+
const result = await lookupProjectByRemote(candidate, host, token, verbose, allowInsecureHttp);
|
|
145
146
|
if (result === "not-found")
|
|
146
147
|
continue;
|
|
147
148
|
return result;
|
|
@@ -164,7 +165,7 @@ export async function enrichCommitProjectAttribution(payloads, options) {
|
|
|
164
165
|
const repoName = payload.metadata?.repo_name;
|
|
165
166
|
if (!repoName)
|
|
166
167
|
continue;
|
|
167
|
-
const result = await lookupProjectByRepoName(repoName, options.host, options.token, options.verbose ?? false);
|
|
168
|
+
const result = await lookupProjectByRepoName(repoName, options.host, options.token, options.verbose ?? false, options.allowInsecureHttp === true);
|
|
168
169
|
if (result && typeof result === "object" && "project_id" in result) {
|
|
169
170
|
payload.project_id = result.project_id;
|
|
170
171
|
if (options.verbose) {
|
|
@@ -173,7 +174,18 @@ export async function enrichCommitProjectAttribution(payloads, options) {
|
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
|
-
export async function lookupProjectByRemote(gitRemote, host, token, verbose) {
|
|
177
|
+
export async function lookupProjectByRemote(gitRemote, host, token, verbose, allowInsecureHttp = false) {
|
|
178
|
+
const transportSecurity = evaluateTransportSecurity(host, {
|
|
179
|
+
allowInsecureHttp,
|
|
180
|
+
label: "DB90 project-lookup host",
|
|
181
|
+
});
|
|
182
|
+
if (!transportSecurity.ok) {
|
|
183
|
+
console.error(`Blocked project lookup — ${transportSecurity.error}`);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (transportSecurity.warning) {
|
|
187
|
+
console.error(`Warning: ${transportSecurity.warning}`);
|
|
188
|
+
}
|
|
177
189
|
const url = `${host.replace(/\/$/, "")}/api/v1/projects/lookup?git_remote=${encodeURIComponent(gitRemote)}`;
|
|
178
190
|
try {
|
|
179
191
|
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -9,4 +9,5 @@ export interface TransportSecurityOptions {
|
|
|
9
9
|
allowInsecureHttp: boolean;
|
|
10
10
|
label: string;
|
|
11
11
|
}
|
|
12
|
+
export declare function isLoopbackHost(hostname: string): boolean;
|
|
12
13
|
export declare function evaluateTransportSecurity(rawUrl: string, options: TransportSecurityOptions): TransportSecurityResult;
|
|
@@ -8,7 +8,7 @@ function isIpv4Loopback(hostname) {
|
|
|
8
8
|
octet <= 255 &&
|
|
9
9
|
String(octet) === parts[index]) && octets[0] === 127;
|
|
10
10
|
}
|
|
11
|
-
function isLoopbackHost(hostname) {
|
|
11
|
+
export function isLoopbackHost(hostname) {
|
|
12
12
|
const normalized = hostname.toLowerCase();
|
|
13
13
|
return normalized === "localhost" ||
|
|
14
14
|
normalized === "::1" ||
|
package/dist/readers/claude.d.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import type { IngestPayload } from "../lib/index.js";
|
|
2
2
|
import { type PricingTable } from "../pricing.js";
|
|
3
3
|
import { type RiskLevel } from "../risk-scanner.js";
|
|
4
|
+
export type ClaudeDerivativeEventType = "edit" | "commit" | "test" | "tool_use";
|
|
5
|
+
export interface ClaudeToolUseBlock {
|
|
6
|
+
id?: string;
|
|
7
|
+
name: string;
|
|
8
|
+
input?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface ClaudeCollectedToolUse {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
eventType: ClaudeDerivativeEventType;
|
|
14
|
+
summary: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function classifyToolUse(block: ClaudeToolUseBlock): ClaudeDerivativeEventType | null;
|
|
17
|
+
export declare function scrubBashCommand(cmd: string): string;
|
|
18
|
+
export declare function summarizeToolUse(block: ClaudeToolUseBlock): string;
|
|
4
19
|
/** True when prompt text alone matches known local-command injection markers. */
|
|
5
20
|
export declare function isClaudeLocalCommandNoisePrompt(promptText: string): boolean;
|
|
6
21
|
/**
|
|
@@ -41,9 +56,13 @@ export interface ClaudeTranscriptTurn {
|
|
|
41
56
|
riskLevel: RiskLevel;
|
|
42
57
|
riskScore: number;
|
|
43
58
|
riskCategories: string[];
|
|
59
|
+
toolUses: ClaudeCollectedToolUse[];
|
|
60
|
+
navToolCalls: number;
|
|
61
|
+
totalToolCalls: number;
|
|
62
|
+
messageIds: string[];
|
|
44
63
|
}
|
|
45
|
-
/** Payload shape
|
|
46
|
-
export interface
|
|
64
|
+
/** Payload shape for the parent chat turn (carries full token cost). */
|
|
65
|
+
export interface ClaudePayload extends IngestPayload {
|
|
47
66
|
tool_name: "claude_code";
|
|
48
67
|
event_type: "chat";
|
|
49
68
|
model?: string;
|
|
@@ -57,7 +76,7 @@ export interface Db90Payload extends IngestPayload {
|
|
|
57
76
|
session_id: string;
|
|
58
77
|
claude_session_id: string;
|
|
59
78
|
transcript_source: "claude_jsonl";
|
|
60
|
-
model
|
|
79
|
+
model?: string | null;
|
|
61
80
|
base_input_tokens: number;
|
|
62
81
|
output_tokens: number;
|
|
63
82
|
cache_write_tokens: number;
|
|
@@ -68,10 +87,39 @@ export interface Db90Payload extends IngestPayload {
|
|
|
68
87
|
prompt_text?: string;
|
|
69
88
|
assistant_text?: string;
|
|
70
89
|
scannable: true;
|
|
90
|
+
cost_model: "token_count";
|
|
91
|
+
nav_tool_calls: number;
|
|
92
|
+
total_tool_calls: number;
|
|
93
|
+
message_ids?: string[];
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Payload shape for derivative tool-use children (cost_usd: 0, no tokens). */
|
|
97
|
+
export interface ClaudeDerivativePayload extends IngestPayload {
|
|
98
|
+
tool_name: "claude_code";
|
|
99
|
+
event_type: ClaudeDerivativeEventType;
|
|
100
|
+
cost_usd: 0;
|
|
101
|
+
occurred_at: string;
|
|
102
|
+
model?: string;
|
|
103
|
+
project_id?: string;
|
|
104
|
+
metadata: {
|
|
105
|
+
session_id: string;
|
|
106
|
+
claude_session_id: string;
|
|
107
|
+
transcript_source: "claude_jsonl";
|
|
108
|
+
cost_model: "derivative";
|
|
109
|
+
parent_session_id: string;
|
|
110
|
+
tool_name_inner: string;
|
|
111
|
+
tool_use_id: string;
|
|
112
|
+
summary: string;
|
|
113
|
+
scannable: false;
|
|
114
|
+
risk_level: "none";
|
|
115
|
+
risk_categories: string[];
|
|
116
|
+
risk_score: 0;
|
|
71
117
|
};
|
|
72
118
|
}
|
|
119
|
+
/** Union of all Claude transcript payloads expected by the ingest API. */
|
|
120
|
+
export type ClaudeMappedPayload = ClaudePayload | ClaudeDerivativePayload;
|
|
73
121
|
/** Options for mapTranscriptTurn. */
|
|
74
|
-
export interface
|
|
122
|
+
export interface ToClaudePayloadOptions {
|
|
75
123
|
projectId?: string | null;
|
|
76
124
|
pricing?: PricingTable;
|
|
77
125
|
}
|
|
@@ -79,5 +127,5 @@ export interface ToDb90PayloadOptions {
|
|
|
79
127
|
export declare function findTranscriptFiles(baseDirs?: string[]): string[];
|
|
80
128
|
/** Streams a JSONL file and splits Claude transcripts into individual turns. */
|
|
81
129
|
export declare function parseTranscriptFile(filePath: string, verbose?: boolean): Promise<ClaudeTranscriptTurn[]>;
|
|
82
|
-
/** Converts a Claude transcript turn to
|
|
83
|
-
export declare function mapTranscriptTurn(turn: ClaudeTranscriptTurn, options?:
|
|
130
|
+
/** Converts a Claude transcript turn to parent chat and derivative tool-use payloads. */
|
|
131
|
+
export declare function mapTranscriptTurn(turn: ClaudeTranscriptTurn, options?: ToClaudePayloadOptions): ClaudeMappedPayload[];
|