@agentstrack/collector 0.1.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/LICENSE +202 -0
  3. package/README.md +779 -0
  4. package/dist/adapters/account.d.ts +22 -0
  5. package/dist/adapters/account.js +142 -0
  6. package/dist/adapters/account.js.map +1 -0
  7. package/dist/adapters/claude.d.ts +49 -0
  8. package/dist/adapters/claude.js +259 -0
  9. package/dist/adapters/claude.js.map +1 -0
  10. package/dist/adapters/codex.d.ts +64 -0
  11. package/dist/adapters/codex.js +350 -0
  12. package/dist/adapters/codex.js.map +1 -0
  13. package/dist/adapters/opencode.d.ts +79 -0
  14. package/dist/adapters/opencode.js +338 -0
  15. package/dist/adapters/opencode.js.map +1 -0
  16. package/dist/adapters/types.d.ts +97 -0
  17. package/dist/adapters/types.js +20 -0
  18. package/dist/adapters/types.js.map +1 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +450 -0
  21. package/dist/cli.js.map +1 -0
  22. package/dist/commands/service.d.ts +3 -0
  23. package/dist/commands/service.js +86 -0
  24. package/dist/commands/service.js.map +1 -0
  25. package/dist/config.d.ts +67 -0
  26. package/dist/config.js +97 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/daemon.d.ts +57 -0
  29. package/dist/daemon.js +368 -0
  30. package/dist/daemon.js.map +1 -0
  31. package/dist/git/commits.d.ts +34 -0
  32. package/dist/git/commits.js +85 -0
  33. package/dist/git/commits.js.map +1 -0
  34. package/dist/git/repo.d.ts +36 -0
  35. package/dist/git/repo.js +141 -0
  36. package/dist/git/repo.js.map +1 -0
  37. package/dist/index.d.ts +12 -0
  38. package/dist/index.js +12 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/privacy/mode.d.ts +11 -0
  41. package/dist/privacy/mode.js +17 -0
  42. package/dist/privacy/mode.js.map +1 -0
  43. package/dist/privacy/paths.d.ts +11 -0
  44. package/dist/privacy/paths.js +30 -0
  45. package/dist/privacy/paths.js.map +1 -0
  46. package/dist/privacy/pipeline.d.ts +26 -0
  47. package/dist/privacy/pipeline.js +112 -0
  48. package/dist/privacy/pipeline.js.map +1 -0
  49. package/dist/privacy/redact.d.ts +29 -0
  50. package/dist/privacy/redact.js +58 -0
  51. package/dist/privacy/redact.js.map +1 -0
  52. package/dist/queue/spool.d.ts +47 -0
  53. package/dist/queue/spool.js +152 -0
  54. package/dist/queue/spool.js.map +1 -0
  55. package/dist/queue/tailer.d.ts +25 -0
  56. package/dist/queue/tailer.js +48 -0
  57. package/dist/queue/tailer.js.map +1 -0
  58. package/dist/schema.d.ts +76 -0
  59. package/dist/schema.js +46 -0
  60. package/dist/schema.js.map +1 -0
  61. package/dist/sessions/title.d.ts +2 -0
  62. package/dist/sessions/title.js +11 -0
  63. package/dist/sessions/title.js.map +1 -0
  64. package/dist/transport/client.d.ts +76 -0
  65. package/dist/transport/client.js +80 -0
  66. package/dist/transport/client.js.map +1 -0
  67. package/package.json +42 -0
@@ -0,0 +1,85 @@
1
+ import { commitsSince, findGitRoot } from './repo.js';
2
+ /** Nothing older than this is attributed to a session, however far back its transcript runs. */
3
+ const MAX_LOOKBACK_MS = 24 * 60 * 60 * 1000;
4
+ export class GitCommitWatcher {
5
+ /** One entry per repo: the session most recently active in it. */
6
+ repos = new Map();
7
+ /** SHAs already emitted, so a commit is reported once and not once per scan. */
8
+ emitted = new Set();
9
+ /** cwd -> git root, so a repeated cwd costs no filesystem walk. */
10
+ roots = new Map();
11
+ /** Records which repo each session is working in. Cheap: no git process. */
12
+ observe(events) {
13
+ for (const item of events) {
14
+ const cwd = item.cwd;
15
+ if (!cwd)
16
+ continue;
17
+ let root = this.roots.get(cwd);
18
+ if (root === undefined) {
19
+ root = findGitRoot(cwd);
20
+ this.roots.set(cwd, root);
21
+ }
22
+ if (!root)
23
+ continue;
24
+ const occurredAt = new Date(item.event.occurred_at);
25
+ const at = Number.isNaN(occurredAt.getTime()) ? new Date() : occurredAt;
26
+ const existing = this.repos.get(root);
27
+ // A new session in this repo takes it over; the same session only ever
28
+ // widens its window backwards.
29
+ if (existing && existing.sessionId === item.event.session_id) {
30
+ if (at < existing.since)
31
+ existing.since = at;
32
+ continue;
33
+ }
34
+ this.repos.set(root, {
35
+ gitRoot: root,
36
+ sessionId: item.event.session_id,
37
+ agent: item.event.agent,
38
+ agentVersion: item.event.agent_version,
39
+ since: at,
40
+ });
41
+ }
42
+ }
43
+ /**
44
+ * One `git log` per watched repo, at most once per call. Repos are dropped
45
+ * afterwards: a repo is re-armed by the next event from it, so an idle
46
+ * project costs nothing.
47
+ */
48
+ async poll(now = new Date()) {
49
+ const watched = [...this.repos.values()];
50
+ this.repos.clear();
51
+ const events = [];
52
+ for (const repo of watched) {
53
+ const since = new Date(Math.max(repo.since.getTime(), now.getTime() - MAX_LOOKBACK_MS));
54
+ for (const commit of await commitsSince(repo.gitRoot, since)) {
55
+ const key = `${repo.gitRoot}:${commit.sha}`;
56
+ if (this.emitted.has(key))
57
+ continue;
58
+ this.emitted.add(key);
59
+ events.push({
60
+ event: {
61
+ occurred_at: commit.committedAt,
62
+ session_id: repo.sessionId,
63
+ agent: repo.agent,
64
+ agent_version: repo.agentVersion,
65
+ event_type: 'git.commit',
66
+ payload: {
67
+ sha: commit.sha,
68
+ committed_at: commit.committedAt,
69
+ additions: commit.additions,
70
+ deletions: commit.deletions,
71
+ files_changed: commit.filesChanged,
72
+ },
73
+ },
74
+ cwd: repo.gitRoot,
75
+ });
76
+ }
77
+ }
78
+ // The set only guards against re-emitting inside one process; a long-lived
79
+ // daemon should not grow it forever.
80
+ if (this.emitted.size > 5000)
81
+ this.emitted.clear();
82
+ return events;
83
+ }
84
+ }
85
+ //# sourceMappingURL=commits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commits.js","sourceRoot":"","sources":["../../src/git/commits.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { RepoContext } from '../schema.js';
2
+ /**
3
+ * Git enrichment.
4
+ *
5
+ * Branch and remote are read straight out of .git — two file reads beat
6
+ * spawning a process per event. `git log` is only shelled out for diffstats,
7
+ * which cannot be read from a file.
8
+ *
9
+ * The remote URL is never transmitted: only a SHA-256 of its normalized form,
10
+ * which is enough to correlate a repo across machines without disclosing it.
11
+ */
12
+ export declare function findGitRoot(startDir: string): string | null;
13
+ export declare function readBranch(gitRoot: string): string | undefined;
14
+ export declare function readRemote(gitRoot: string): string | undefined;
15
+ /**
16
+ * Collapses ssh and https forms of one repository onto a single identity, so
17
+ * the same repo cloned two ways still correlates. Mirrors the server's
18
+ * normalizeRemote — they must agree or nothing matches.
19
+ */
20
+ export declare function normalizeRemote(remote: string): string;
21
+ export declare function hashRemote(remote: string): string;
22
+ /** owner/name parsed out of a normalized remote, for display only. */
23
+ export declare function ownerAndName(remote: string): {
24
+ owner?: string;
25
+ name?: string;
26
+ };
27
+ export declare function describeRepo(cwd: string): RepoContext | undefined;
28
+ export interface CommitInfo {
29
+ sha: string;
30
+ committedAt: string;
31
+ additions: number;
32
+ deletions: number;
33
+ filesChanged: number;
34
+ }
35
+ /** Commits authored in this repo within a time window. */
36
+ export declare function commitsSince(gitRoot: string, since: Date): Promise<CommitInfo[]>;
@@ -0,0 +1,141 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ const exec = promisify(execFile);
7
+ /**
8
+ * Git enrichment.
9
+ *
10
+ * Branch and remote are read straight out of .git — two file reads beat
11
+ * spawning a process per event. `git log` is only shelled out for diffstats,
12
+ * which cannot be read from a file.
13
+ *
14
+ * The remote URL is never transmitted: only a SHA-256 of its normalized form,
15
+ * which is enough to correlate a repo across machines without disclosing it.
16
+ */
17
+ export function findGitRoot(startDir) {
18
+ let dir = startDir;
19
+ for (let i = 0; i < 40; i++) {
20
+ if (existsSync(join(dir, '.git')))
21
+ return dir;
22
+ const parent = dirname(dir);
23
+ if (parent === dir)
24
+ return null;
25
+ dir = parent;
26
+ }
27
+ return null;
28
+ }
29
+ export function readBranch(gitRoot) {
30
+ try {
31
+ const head = readFileSync(join(gitRoot, '.git', 'HEAD'), 'utf8').trim();
32
+ const match = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
33
+ // A detached HEAD holds a raw SHA and has no branch name.
34
+ return match?.[1];
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ export function readRemote(gitRoot) {
41
+ try {
42
+ const config = readFileSync(join(gitRoot, '.git', 'config'), 'utf8');
43
+ // Prefer origin; fall back to the first remote defined.
44
+ const origin = /\[remote "origin"\][^[]*?url\s*=\s*(.+)/s.exec(config);
45
+ if (origin?.[1])
46
+ return origin[1].split('\n')[0].trim();
47
+ const any = /\[remote "[^"]+"\][^[]*?url\s*=\s*(.+)/s.exec(config);
48
+ return any?.[1]?.split('\n')[0]?.trim();
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ }
54
+ /**
55
+ * Collapses ssh and https forms of one repository onto a single identity, so
56
+ * the same repo cloned two ways still correlates. Mirrors the server's
57
+ * normalizeRemote — they must agree or nothing matches.
58
+ */
59
+ export function normalizeRemote(remote) {
60
+ let s = remote.trim().replace(/\.git$/, '').replace(/^git\+/, '');
61
+ const ssh = /^(?:ssh:\/\/)?(?:[^@]+@)?([^:/]+)[:/](.+)$/.exec(s);
62
+ if (ssh && !s.startsWith('http'))
63
+ return `${ssh[1].toLowerCase()}/${ssh[2].toLowerCase()}`;
64
+ try {
65
+ const u = new URL(s);
66
+ return `${u.host.toLowerCase()}${u.pathname.replace(/\/$/, '').toLowerCase()}`;
67
+ }
68
+ catch {
69
+ return s.toLowerCase();
70
+ }
71
+ }
72
+ export function hashRemote(remote) {
73
+ return createHash('sha256').update(normalizeRemote(remote)).digest('hex');
74
+ }
75
+ /** owner/name parsed out of a normalized remote, for display only. */
76
+ export function ownerAndName(remote) {
77
+ const parts = normalizeRemote(remote).split('/');
78
+ if (parts.length < 3)
79
+ return {};
80
+ return { owner: parts[parts.length - 2], name: parts[parts.length - 1] };
81
+ }
82
+ export function describeRepo(cwd) {
83
+ const root = findGitRoot(cwd);
84
+ if (!root)
85
+ return { project_path: cwd, project_name: basename(cwd) };
86
+ const remote = readRemote(root);
87
+ const context = {
88
+ branch: readBranch(root),
89
+ project_path: root,
90
+ project_name: basename(root),
91
+ };
92
+ if (remote) {
93
+ context.remote_hash = hashRemote(remote);
94
+ const { owner, name } = ownerAndName(remote);
95
+ context.remote_owner = owner;
96
+ context.remote_name = name;
97
+ }
98
+ return context;
99
+ }
100
+ /**
101
+ * git prints local time with an offset; the wire schema only accepts UTC, so an
102
+ * un-normalized timestamp gets the whole event rejected at ingest.
103
+ */
104
+ function toUtcIso(value) {
105
+ if (!value)
106
+ return undefined;
107
+ const date = new Date(value);
108
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
109
+ }
110
+ /** Commits authored in this repo within a time window. */
111
+ export async function commitsSince(gitRoot, since) {
112
+ try {
113
+ const { stdout } = await exec('git', ['log', `--since=${since.toISOString()}`, '--numstat', '--format=%H%x00%cI', '--no-merges'], { cwd: gitRoot, timeout: 10_000, maxBuffer: 4 * 1024 * 1024 });
114
+ const commits = [];
115
+ let current = null;
116
+ for (const line of stdout.split('\n')) {
117
+ if (line.includes('\0')) {
118
+ if (current)
119
+ commits.push(current);
120
+ const [sha, committedAt] = line.split('\0');
121
+ current = { sha: sha, committedAt: toUtcIso(committedAt) ?? since.toISOString(), additions: 0, deletions: 0, filesChanged: 0 };
122
+ continue;
123
+ }
124
+ if (!current || !line.trim())
125
+ continue;
126
+ const [added, removed] = line.split('\t');
127
+ // Binary files show '-' rather than a count.
128
+ current.additions += Number(added) || 0;
129
+ current.deletions += Number(removed) || 0;
130
+ current.filesChanged += 1;
131
+ }
132
+ if (current)
133
+ commits.push(current);
134
+ return commits;
135
+ }
136
+ catch {
137
+ // No git binary, not a repo, or a timeout — enrichment is best-effort.
138
+ return [];
139
+ }
140
+ }
141
+ //# sourceMappingURL=repo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo.js","sourceRoot":"","sources":["../../src/git/repo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export * from './schema.js';
2
+ export * from './config.js';
3
+ export { Collector, buildAdapters, listTranscripts, VERSION } from './daemon.js';
4
+ export { ClaudeCodeAdapter } from './adapters/claude.js';
5
+ export { CodexAdapter } from './adapters/codex.js';
6
+ export { OpenCodeAdapter } from './adapters/opencode.js';
7
+ export { readClaudeAccount, readOpenCodeAccounts } from './adapters/account.js';
8
+ export type { AccountIdentity, AgentAdapter, DetectionResult, HealthStatus, NormalizedEvent, PollContext, } from './adapters/types.js';
9
+ export { redact, BUILTIN_RULES } from './privacy/redact.js';
10
+ export { applyPrivacy } from './privacy/pipeline.js';
11
+ export { Spool } from './queue/spool.js';
12
+ export { ApiClient } from './transport/client.js';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export * from './schema.js';
2
+ export * from './config.js';
3
+ export { Collector, buildAdapters, listTranscripts, VERSION } from './daemon.js';
4
+ export { ClaudeCodeAdapter } from './adapters/claude.js';
5
+ export { CodexAdapter } from './adapters/codex.js';
6
+ export { OpenCodeAdapter } from './adapters/opencode.js';
7
+ export { readClaudeAccount, readOpenCodeAccounts } from './adapters/account.js';
8
+ export { redact, BUILTIN_RULES } from './privacy/redact.js';
9
+ export { applyPrivacy } from './privacy/pipeline.js';
10
+ export { Spool } from './queue/spool.js';
11
+ export { ApiClient } from './transport/client.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAShF,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { PrivacyMode } from '../schema.js';
2
+ /**
3
+ * The org policy is a CEILING, never a floor.
4
+ *
5
+ * A developer who has chosen `metadata` locally must keep it even if their
6
+ * organization permits `full`. Assigning the server's value outright silently
7
+ * widens what leaves the machine, which is the one thing this tool must never
8
+ * do — so both `login` and the daemon go through here.
9
+ */
10
+ export declare function clampPrivacyMode(local: PrivacyMode, orgCeiling: PrivacyMode): PrivacyMode;
11
+ export declare function isStricter(a: PrivacyMode, b: PrivacyMode): boolean;
@@ -0,0 +1,17 @@
1
+ /** Least to most disclosing. */
2
+ const ORDER = { metadata: 0, analytics: 1, full: 2 };
3
+ /**
4
+ * The org policy is a CEILING, never a floor.
5
+ *
6
+ * A developer who has chosen `metadata` locally must keep it even if their
7
+ * organization permits `full`. Assigning the server's value outright silently
8
+ * widens what leaves the machine, which is the one thing this tool must never
9
+ * do — so both `login` and the daemon go through here.
10
+ */
11
+ export function clampPrivacyMode(local, orgCeiling) {
12
+ return ORDER[local] <= ORDER[orgCeiling] ? local : orgCeiling;
13
+ }
14
+ export function isStricter(a, b) {
15
+ return ORDER[a] < ORDER[b];
16
+ }
17
+ //# sourceMappingURL=mode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode.js","sourceRoot":"","sources":["../../src/privacy/mode.ts"],"names":[],"mappings":"AAEA,gCAAgC;AAChC,MAAM,KAAK,GAAgC,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAElF;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAkB,EAAE,UAAuB;IAC1E,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,CAAc,EAAE,CAAc;IACvD,OAAO,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Path handling per privacy mode.
3
+ *
4
+ * An absolute path is itself disclosure — it leaks usernames, client names and
5
+ * directory structure. `relative` (the default) keeps paths useful for
6
+ * analytics while dropping everything above the project root.
7
+ */
8
+ export type PathMode = 'never' | 'relative' | 'absolute';
9
+ export declare function normalizePath(path: string, projectRoot: string | undefined, mode: PathMode): string | null;
10
+ /** True when a project is on the user's exclusion list. */
11
+ export declare function isExcluded(projectPath: string, excluded: string[]): boolean;
@@ -0,0 +1,30 @@
1
+ import { relative, sep } from 'node:path';
2
+ import { homedir } from 'node:os';
3
+ export function normalizePath(path, projectRoot, mode) {
4
+ if (mode === 'never')
5
+ return null;
6
+ if (mode === 'absolute')
7
+ return path;
8
+ if (projectRoot && path.startsWith(projectRoot)) {
9
+ const rel = relative(projectRoot, path);
10
+ return rel === '' ? '.' : rel;
11
+ }
12
+ // Outside the project: keep the shape, drop the identity.
13
+ const home = homedir();
14
+ if (path.startsWith(home))
15
+ return `~${path.slice(home.length)}`;
16
+ // Unknown absolute path — keep only the last two segments so the file type
17
+ // is still visible without revealing the tree it sits in.
18
+ const parts = path.split(sep).filter(Boolean);
19
+ return parts.length <= 2 ? path : `…${sep}${parts.slice(-2).join(sep)}`;
20
+ }
21
+ /** True when a project is on the user's exclusion list. */
22
+ export function isExcluded(projectPath, excluded) {
23
+ const home = homedir();
24
+ const normalized = projectPath.replace(/\/+$/, '');
25
+ return excluded.some((pattern) => {
26
+ const expanded = pattern.replace(/^~/, home).replace(/\/+$/, '');
27
+ return normalized === expanded || normalized.startsWith(`${expanded}${sep}`);
28
+ });
29
+ }
30
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/privacy/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAWlC,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,WAA+B,EAAE,IAAc;IACzF,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,IAAI,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IAErC,IAAI,WAAW,IAAI,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChD,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QACxC,OAAO,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IAChC,CAAC;IAED,0DAA0D;IAC1D,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;IAEhE,2EAA2E;IAC3E,0DAA0D;IAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9C,OAAO,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AAC1E,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,UAAU,CAAC,WAAmB,EAAE,QAAkB;IAChE,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACnD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;QAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACjE,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,GAAG,EAAE,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { Config } from '../config.js';
2
+ import type { EventEnvelope } from '../schema.js';
3
+ /**
4
+ * The privacy pipeline from BLUEPRINT §9.3, applied to every event before it
5
+ * enters the upload spool:
6
+ *
7
+ * raw event -> policy lookup -> secret detection -> path/content redaction
8
+ * -> optional local classification -> optional local summary
9
+ * -> raw content discard -> normalized event
10
+ *
11
+ * Enforced here, on the developer's machine. By the time an event reaches the
12
+ * spool there is nothing left to leak.
13
+ */
14
+ export interface PipelineContext {
15
+ config: Config;
16
+ projectRoot?: string;
17
+ orgRules?: {
18
+ pattern: string;
19
+ replacement: string;
20
+ }[];
21
+ }
22
+ export interface PipelineResult {
23
+ event: EventEnvelope;
24
+ redactions: string[];
25
+ }
26
+ export declare function applyPrivacy(event: EventEnvelope, ctx: PipelineContext): PipelineResult;
@@ -0,0 +1,112 @@
1
+ import { commandName, compileRules, redact } from './redact.js';
2
+ import { normalizePath } from './paths.js';
3
+ /** Payload keys that carry free text and must always be scanned for secrets. */
4
+ const TEXT_KEYS = ['prompt_text', 'derived_title', 'message', 'command'];
5
+ export function applyPrivacy(event, ctx) {
6
+ const mode = ctx.config.privacy.mode;
7
+ const policy = ctx.config.privacy;
8
+ const extraRules = compileRules(ctx.orgRules ?? []);
9
+ const redactions = [];
10
+ const payload = { ...event.payload };
11
+ // --- content: strip anything the mode does not permit -------------------
12
+ if (mode === 'metadata') {
13
+ // Counts and timings only. Titles are derived from prompts, so they go too.
14
+ delete payload['prompt_text'];
15
+ delete payload['derived_title'];
16
+ delete payload['message'];
17
+ // A shell command line IS content: it carries hostnames, client names,
18
+ // usernames and inline passwords. `metadata` promises none of that, so the
19
+ // binary name is all that may travel regardless of shell_arguments.
20
+ if (typeof payload['command'] === 'string') {
21
+ payload['command'] = commandName(payload['command']);
22
+ }
23
+ }
24
+ else if (mode === 'analytics') {
25
+ // Locally derived summaries may travel; the prompt itself never does.
26
+ delete payload['prompt_text'];
27
+ // `prompts: never` is stricter than the mode and must still be honoured —
28
+ // it previously only took effect in `full` mode, making it a privacy
29
+ // control that silently did nothing.
30
+ if (policy.prompts === 'never')
31
+ delete payload['derived_title'];
32
+ }
33
+ else if (policy.prompts !== 'full') {
34
+ // 'full' mode still honours a stricter prompts policy.
35
+ delete payload['prompt_text'];
36
+ // ...including the locally derived summary. `never` means nothing
37
+ // prompt-derived leaves the machine, in every mode — not just analytics.
38
+ if (policy.prompts === 'never')
39
+ delete payload['derived_title'];
40
+ }
41
+ // --- code content -------------------------------------------------------
42
+ // `code_content: never` is the default and means exactly that: file bodies
43
+ // and diffs are dropped regardless of mode, so opting into `full` prompts
44
+ // does not silently opt into shipping source code.
45
+ if (policy.code_content === 'never') {
46
+ delete payload['content'];
47
+ delete payload['diff'];
48
+ delete payload['old_string'];
49
+ delete payload['new_string'];
50
+ }
51
+ // --- secret detection on whatever text survives -------------------------
52
+ for (const key of TEXT_KEYS) {
53
+ const value = payload[key];
54
+ if (typeof value !== 'string')
55
+ continue;
56
+ const result = redact(value, extraRules);
57
+ payload[key] = result.text;
58
+ redactions.push(...result.redactions);
59
+ }
60
+ // --- paths --------------------------------------------------------------
61
+ if (typeof payload['path'] === 'string') {
62
+ const normalized = normalizePath(payload['path'], ctx.projectRoot, policy.file_paths);
63
+ if (normalized === null)
64
+ delete payload['path'];
65
+ else
66
+ payload['path'] = normalized;
67
+ }
68
+ // `cwd` on session.started IS the project root, so it gets the same treatment
69
+ // as repo.project_path — an absolute path leaks the username, the client name
70
+ // and the directory tree, and it was travelling in metadata mode untouched.
71
+ if (typeof payload['cwd'] === 'string' && policy.file_paths !== 'absolute') {
72
+ delete payload['cwd'];
73
+ }
74
+ const repo = payload['repo'];
75
+ if (repo && typeof repo === 'object') {
76
+ const r = { ...repo };
77
+ if (policy.file_paths === 'never')
78
+ delete r['project_path'];
79
+ else if (typeof r['project_path'] === 'string' && policy.file_paths === 'relative') {
80
+ // The project root itself is only ever sent as a hash, never a path.
81
+ delete r['project_path'];
82
+ }
83
+ payload['repo'] = r;
84
+ }
85
+ // --- account identity ---------------------------------------------------
86
+ // `key` is an opaque provider account id and always travels: it is the only
87
+ // thing that keeps two accounts' sessions apart, and in `metadata` mode the
88
+ // account must still show up as a distinct (if anonymous) identity rather
89
+ // than merging into everyone else's. `label` is an email or display name and
90
+ // `org` is an employer — both name a human, so they are content and are
91
+ // dropped below `analytics` exactly like a prompt is.
92
+ const account = payload['account'];
93
+ if (account && typeof account === 'object' && !Array.isArray(account)) {
94
+ const a = account;
95
+ const trimmed = { key: a['key'] };
96
+ if (a['provider'] !== undefined)
97
+ trimmed['provider'] = a['provider'];
98
+ if (mode !== 'metadata') {
99
+ if (a['label'] !== undefined)
100
+ trimmed['label'] = a['label'];
101
+ if (a['org'] !== undefined)
102
+ trimmed['org'] = a['org'];
103
+ }
104
+ payload['account'] = trimmed;
105
+ }
106
+ // --- shell arguments ----------------------------------------------------
107
+ if (typeof payload['command'] === 'string' && policy.shell_arguments === 'never') {
108
+ payload['command'] = payload['command'].split(/\s+/)[0] ?? '';
109
+ }
110
+ return { event: { ...event, payload }, redactions };
111
+ }
112
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../src/privacy/pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Secret detection and redaction.
3
+ *
4
+ * This runs on the developer's machine, before anything is uploaded. It is the
5
+ * first line of defence, not the last: in `metadata` mode there is simply no
6
+ * content to leak because it was discarded here.
7
+ *
8
+ * Patterns are ordered most-specific first so a token that matches two rules
9
+ * is labelled by the more precise one.
10
+ */
11
+ export interface RedactionRule {
12
+ name: string;
13
+ pattern: RegExp;
14
+ replacement: string;
15
+ }
16
+ export declare const BUILTIN_RULES: RedactionRule[];
17
+ export interface RedactionResult {
18
+ text: string;
19
+ /** Names of rules that fired, for the trust metrics in BLUEPRINT §14. */
20
+ redactions: string[];
21
+ }
22
+ export declare function redact(input: string, extraRules?: RedactionRule[]): RedactionResult;
23
+ /** Compiles org-supplied patterns, skipping any that do not compile. */
24
+ export declare function compileRules(rules: {
25
+ pattern: string;
26
+ replacement: string;
27
+ }[]): RedactionRule[];
28
+ /** Extracts the binary name, dropping arguments that may hold secrets. */
29
+ export declare function commandName(commandLine: string): string;
@@ -0,0 +1,58 @@
1
+ export const BUILTIN_RULES = [
2
+ { name: 'anthropic_key', pattern: /sk-ant-[A-Za-z0-9_-]{20,}/g, replacement: '[REDACTED:anthropic_key]' },
3
+ { name: 'openai_key', pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g, replacement: '[REDACTED:openai_key]' },
4
+ { name: 'github_token', pattern: /gh[pousr]_[A-Za-z0-9]{16,}/g, replacement: '[REDACTED:github_token]' },
5
+ { name: 'github_pat', pattern: /github_pat_[A-Za-z0-9_]{20,}/g, replacement: '[REDACTED:github_pat]' },
6
+ { name: 'slack_token', pattern: /xox[baprs]-[A-Za-z0-9-]{10,}/g, replacement: '[REDACTED:slack_token]' },
7
+ { name: 'stripe_key', pattern: /[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/g, replacement: '[REDACTED:stripe_key]' },
8
+ { name: 'aws_access_key', pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, replacement: '[REDACTED:aws_access_key]' },
9
+ { name: 'google_api_key', pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, replacement: '[REDACTED:google_api_key]' },
10
+ { name: 'agentstrack_key', pattern: /\bat_(?:live|test)_[a-f0-9]{16}_[A-Za-z0-9_-]{20,}/g, replacement: '[REDACTED:agentstrack_key]' },
11
+ { name: 'private_key', pattern: /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----/g, replacement: '[REDACTED:private_key]' },
12
+ { name: 'jwt', pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, replacement: '[REDACTED:jwt]' },
13
+ { name: 'bearer_header', pattern: /\b[Bb]earer\s+[A-Za-z0-9._~+/-]{20,}=*/g, replacement: 'Bearer [REDACTED]' },
14
+ { name: 'basic_auth_url', pattern: /(\b[a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/g, replacement: '$1[REDACTED]@' },
15
+ { name: 'env_assignment', pattern: /\b([A-Z_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|APIKEY|API_KEY|PRIVATE_KEY|ACCESS_KEY)[A-Z_]*)\s*=\s*("[^"]*"|'[^']*'|\S+)/g, replacement: '$1=[REDACTED]' },
16
+ // mysql/psql style inline credentials: -pSECRET, --password=SECRET. Extremely
17
+ // common in agent shell calls and missed by every key-shaped rule above.
18
+ { name: 'inline_password_flag', pattern: /(--password[= ]|(?<![\w-])-p)(?!\s)("[^"]*"|'[^']*'|\S+)/g, replacement: '$1[REDACTED]' },
19
+ { name: 'generic_hex_secret', pattern: /\b[a-f0-9]{40,}\b/g, replacement: '[REDACTED:hex]' },
20
+ ];
21
+ export function redact(input, extraRules = []) {
22
+ let text = input;
23
+ const redactions = [];
24
+ for (const rule of [...BUILTIN_RULES, ...extraRules]) {
25
+ // Fresh lastIndex per call: these regexes are global and module-level, so
26
+ // reusing them statefully across calls would skip matches.
27
+ rule.pattern.lastIndex = 0;
28
+ if (!rule.pattern.test(text))
29
+ continue;
30
+ rule.pattern.lastIndex = 0;
31
+ text = text.replace(rule.pattern, rule.replacement);
32
+ redactions.push(rule.name);
33
+ }
34
+ return { text, redactions };
35
+ }
36
+ /** Compiles org-supplied patterns, skipping any that do not compile. */
37
+ export function compileRules(rules) {
38
+ const compiled = [];
39
+ for (const [index, rule] of rules.entries()) {
40
+ try {
41
+ compiled.push({
42
+ name: `org_rule_${index}`,
43
+ pattern: new RegExp(rule.pattern, 'g'),
44
+ replacement: rule.replacement,
45
+ });
46
+ }
47
+ catch {
48
+ // A malformed server-side rule must not stop the collector entirely.
49
+ }
50
+ }
51
+ return compiled;
52
+ }
53
+ /** Extracts the binary name, dropping arguments that may hold secrets. */
54
+ export function commandName(commandLine) {
55
+ const first = commandLine.trim().split(/\s+/)[0] ?? '';
56
+ return first.split('/').pop() ?? first;
57
+ }
58
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/privacy/redact.ts"],"names":[],"mappings":"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"}