@nocoo/otter 1.0.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 (81) hide show
  1. package/dist/bin.d.ts +3 -0
  2. package/dist/bin.d.ts.map +1 -0
  3. package/dist/bin.js +5 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +365 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/collectors/applications.d.ts +19 -0
  10. package/dist/collectors/applications.d.ts.map +1 -0
  11. package/dist/collectors/applications.js +51 -0
  12. package/dist/collectors/applications.js.map +1 -0
  13. package/dist/collectors/base.d.ts +52 -0
  14. package/dist/collectors/base.d.ts.map +1 -0
  15. package/dist/collectors/base.js +186 -0
  16. package/dist/collectors/base.js.map +1 -0
  17. package/dist/collectors/claude-config.d.ts +39 -0
  18. package/dist/collectors/claude-config.d.ts.map +1 -0
  19. package/dist/collectors/claude-config.js +124 -0
  20. package/dist/collectors/claude-config.js.map +1 -0
  21. package/dist/collectors/homebrew.d.ts +16 -0
  22. package/dist/collectors/homebrew.d.ts.map +1 -0
  23. package/dist/collectors/homebrew.js +43 -0
  24. package/dist/collectors/homebrew.js.map +1 -0
  25. package/dist/collectors/index.d.ts +21 -0
  26. package/dist/collectors/index.d.ts.map +1 -0
  27. package/dist/collectors/index.js +28 -0
  28. package/dist/collectors/index.js.map +1 -0
  29. package/dist/collectors/opencode-config.d.ts +21 -0
  30. package/dist/collectors/opencode-config.d.ts.map +1 -0
  31. package/dist/collectors/opencode-config.js +88 -0
  32. package/dist/collectors/opencode-config.js.map +1 -0
  33. package/dist/collectors/shell-config.d.ts +24 -0
  34. package/dist/collectors/shell-config.d.ts.map +1 -0
  35. package/dist/collectors/shell-config.js +133 -0
  36. package/dist/collectors/shell-config.js.map +1 -0
  37. package/dist/commands/config.d.ts +17 -0
  38. package/dist/commands/config.d.ts.map +1 -0
  39. package/dist/commands/config.js +21 -0
  40. package/dist/commands/config.js.map +1 -0
  41. package/dist/commands/scan.d.ts +11 -0
  42. package/dist/commands/scan.d.ts.map +1 -0
  43. package/dist/commands/scan.js +36 -0
  44. package/dist/commands/scan.js.map +1 -0
  45. package/dist/commands/snapshot.d.ts +52 -0
  46. package/dist/commands/snapshot.d.ts.map +1 -0
  47. package/dist/commands/snapshot.js +203 -0
  48. package/dist/commands/snapshot.js.map +1 -0
  49. package/dist/config/manager.d.ts +13 -0
  50. package/dist/config/manager.d.ts.map +1 -0
  51. package/dist/config/manager.js +28 -0
  52. package/dist/config/manager.js.map +1 -0
  53. package/dist/index.d.ts +8 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +8 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/snapshot/builder.d.ts +7 -0
  58. package/dist/snapshot/builder.d.ts.map +1 -0
  59. package/dist/snapshot/builder.js +68 -0
  60. package/dist/snapshot/builder.js.map +1 -0
  61. package/dist/storage/local.d.ts +44 -0
  62. package/dist/storage/local.d.ts.map +1 -0
  63. package/dist/storage/local.js +107 -0
  64. package/dist/storage/local.js.map +1 -0
  65. package/dist/uploader/icons.d.ts +41 -0
  66. package/dist/uploader/icons.d.ts.map +1 -0
  67. package/dist/uploader/icons.js +80 -0
  68. package/dist/uploader/icons.js.map +1 -0
  69. package/dist/uploader/webhook.d.ts +7 -0
  70. package/dist/uploader/webhook.d.ts.map +1 -0
  71. package/dist/uploader/webhook.js +54 -0
  72. package/dist/uploader/webhook.js.map +1 -0
  73. package/dist/utils/icons.d.ts +34 -0
  74. package/dist/utils/icons.d.ts.map +1 -0
  75. package/dist/utils/icons.js +113 -0
  76. package/dist/utils/icons.js.map +1 -0
  77. package/dist/utils/redact.d.ts +41 -0
  78. package/dist/utils/redact.d.ts.map +1 -0
  79. package/dist/utils/redact.js +317 -0
  80. package/dist/utils/redact.js.map +1 -0
  81. package/package.json +45 -0
@@ -0,0 +1,186 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import { join, extname, basename } from "node:path";
3
+ import { redactSecrets } from "../utils/redact.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Constants for safety limits
6
+ // ---------------------------------------------------------------------------
7
+ /** Maximum size (in bytes) for a single file to be collected (512 KB) */
8
+ const MAX_FILE_SIZE_BYTES = 512 * 1024;
9
+ /** Directory names that should always be skipped during recursive collection */
10
+ const EXCLUDED_DIRS = new Set([
11
+ ".git",
12
+ "node_modules",
13
+ "__pycache__",
14
+ ".cache",
15
+ "cache",
16
+ "target",
17
+ "build",
18
+ "dist",
19
+ ".next",
20
+ ".nuxt",
21
+ ".turbo",
22
+ ]);
23
+ /** File extensions that indicate binary / non-text content */
24
+ const BINARY_EXTENSIONS = new Set([
25
+ ".ds_store",
26
+ ".sqlite",
27
+ ".sqlite3",
28
+ ".db",
29
+ ".wasm",
30
+ ".dylib",
31
+ ".so",
32
+ ".dll",
33
+ ".exe",
34
+ ".o",
35
+ ".a",
36
+ ".ico",
37
+ ".png",
38
+ ".jpg",
39
+ ".jpeg",
40
+ ".gif",
41
+ ".webp",
42
+ ".bmp",
43
+ ".tiff",
44
+ ".mp3",
45
+ ".mp4",
46
+ ".mov",
47
+ ".avi",
48
+ ".zip",
49
+ ".tar",
50
+ ".gz",
51
+ ".bz2",
52
+ ".xz",
53
+ ".7z",
54
+ ".rar",
55
+ ".pdf",
56
+ ".ttf",
57
+ ".otf",
58
+ ".woff",
59
+ ".woff2",
60
+ ".eot",
61
+ ]);
62
+ /** File basenames that are always binary regardless of extension */
63
+ const BINARY_BASENAMES = new Set([".ds_store", "thumbs.db", "desktop.ini"]);
64
+ // ---------------------------------------------------------------------------
65
+ // Helpers
66
+ // ---------------------------------------------------------------------------
67
+ /** Check if a file is likely binary based on its name / extension */
68
+ function isBinaryFile(fileName) {
69
+ const lower = fileName.toLowerCase();
70
+ if (BINARY_BASENAMES.has(lower))
71
+ return true;
72
+ const ext = extname(lower);
73
+ return ext !== "" && BINARY_EXTENSIONS.has(ext);
74
+ }
75
+ /**
76
+ * Base class for all collectors. Provides common file reading utilities
77
+ * and standardized result creation.
78
+ */
79
+ export class BaseCollector {
80
+ homeDir;
81
+ constructor(homeDir) {
82
+ this.homeDir = homeDir;
83
+ }
84
+ /** Create an empty result skeleton */
85
+ createResult() {
86
+ return {
87
+ id: this.id,
88
+ label: this.label,
89
+ category: this.category,
90
+ files: [],
91
+ lists: [],
92
+ errors: [],
93
+ durationMs: 0,
94
+ };
95
+ }
96
+ /**
97
+ * Safely read a single file. Returns null if file doesn't exist,
98
+ * can't be read, exceeds size limit, or is binary.
99
+ *
100
+ * @param redact If true, apply credential redaction based on file type
101
+ */
102
+ async safeReadFile(filePath, result, { maxSize = MAX_FILE_SIZE_BYTES, redact = false } = {}) {
103
+ try {
104
+ // Skip binary files
105
+ const fileName = basename(filePath);
106
+ if (isBinaryFile(fileName))
107
+ return null;
108
+ const info = await stat(filePath);
109
+ // Skip files exceeding size limit
110
+ if (info.size > maxSize) {
111
+ result.errors.push(`Skipped ${filePath}: exceeds size limit (${(info.size / 1024).toFixed(0)} KB > ${(maxSize / 1024).toFixed(0)} KB)`);
112
+ return null;
113
+ }
114
+ let content = await readFile(filePath, "utf-8");
115
+ // Apply credential redaction if requested
116
+ if (redact) {
117
+ content = redactSecrets(content, filePath);
118
+ }
119
+ return {
120
+ path: filePath,
121
+ content,
122
+ sizeBytes: info.size,
123
+ };
124
+ }
125
+ catch (err) {
126
+ if (err.code !== "ENOENT") {
127
+ result.errors.push(`Failed to read ${filePath}: ${err.message}`);
128
+ }
129
+ return null;
130
+ }
131
+ }
132
+ /**
133
+ * Recursively collect all files in a directory.
134
+ *
135
+ * Safety features:
136
+ * - Skips excluded directories (.git, node_modules, cache, build output, etc.)
137
+ * - Skips binary files (by extension and known basenames)
138
+ * - Skips files exceeding size limit (default 512 KB)
139
+ */
140
+ async collectDir(dirPath, result, opts = {}) {
141
+ const { filter, maxFileSize = MAX_FILE_SIZE_BYTES, excludeDirs, redact } = opts;
142
+ const files = [];
143
+ try {
144
+ const entries = await readdir(dirPath, { withFileTypes: true });
145
+ for (const entry of entries) {
146
+ const fullPath = join(dirPath, entry.name);
147
+ if (entry.isDirectory()) {
148
+ const dirName = entry.name.toLowerCase();
149
+ // Skip globally excluded directories
150
+ if (EXCLUDED_DIRS.has(dirName))
151
+ continue;
152
+ // Skip caller-specified excluded directories
153
+ if (excludeDirs?.has(dirName))
154
+ continue;
155
+ const subFiles = await this.collectDir(fullPath, result, opts);
156
+ files.push(...subFiles);
157
+ }
158
+ else if (entry.isFile()) {
159
+ if (filter && !filter(fullPath))
160
+ continue;
161
+ const file = await this.safeReadFile(fullPath, result, {
162
+ maxSize: maxFileSize,
163
+ redact,
164
+ });
165
+ if (file)
166
+ files.push(file);
167
+ }
168
+ }
169
+ }
170
+ catch (err) {
171
+ if (err.code !== "ENOENT") {
172
+ result.errors.push(`Failed to read directory ${dirPath}: ${err.message}`);
173
+ }
174
+ }
175
+ return files;
176
+ }
177
+ /** Measure execution time of the collect operation */
178
+ async timed(fn) {
179
+ const result = this.createResult();
180
+ const start = performance.now();
181
+ await fn(result);
182
+ result.durationMs = Math.round(performance.now() - start);
183
+ return result;
184
+ }
185
+ }
186
+ //# sourceMappingURL=base.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.js","sourceRoot":"","sources":["../../src/collectors/base.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import { BaseCollector } from "./base.js";
2
+ import type { CollectorCategory, CollectorResult } from "@otter/core";
3
+ /**
4
+ * Collects Claude Code configuration files with targeted, safe collection.
5
+ *
6
+ * What we collect:
7
+ * - ~/CLAUDE.md (user-level instructions)
8
+ * - ~/.claude/CLAUDE.md, settings.json, stats-cache.json
9
+ * - ~/.claude/plugins/installed_plugins.json, plugins/blocklist.json
10
+ * - ~/.claude/history.jsonl
11
+ * - Conversation metadata summaries (title, timestamps, token counts)
12
+ * from projects/sessions-index.json files — NOT full conversation content
13
+ *
14
+ * What we skip:
15
+ * - debug/, telemetry/, transcripts/, cache/, paste-cache/
16
+ * - shell-snapshots/, session-env/, statsig/
17
+ * - All .jsonl session content files
18
+ * - .git/ directories inside plugins
19
+ * - Binary files, large files
20
+ */
21
+ export declare class ClaudeConfigCollector extends BaseCollector {
22
+ readonly id = "claude-config";
23
+ readonly label = "Claude Code Configuration";
24
+ readonly category: CollectorCategory;
25
+ private readonly slim;
26
+ constructor(homeDir: string, options?: {
27
+ slim?: boolean;
28
+ });
29
+ collect(): Promise<CollectorResult>;
30
+ /**
31
+ * Iterate over all projects/sessions-index.json files and extract
32
+ * lightweight metadata for each conversation session.
33
+ *
34
+ * Returns a synthetic CollectedFile containing JSON with all project
35
+ * summaries — no actual conversation content is included.
36
+ */
37
+ private collectSessionSummaries;
38
+ }
39
+ //# sourceMappingURL=claude-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-config.d.ts","sourceRoot":"","sources":["../../src/collectors/claude-config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EAEhB,MAAM,aAAa,CAAC;AA0CrB;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,qBAAsB,SAAQ,aAAa;IACtD,QAAQ,CAAC,EAAE,mBAAmB;IAC9B,QAAQ,CAAC,KAAK,+BAA+B;IAC7C,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAY;IAEhD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAU;gBAEnB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE;IAKnD,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC;IAmCzC;;;;;;OAMG;YACW,uBAAuB;CA6DtC"}
@@ -0,0 +1,124 @@
1
+ import { join } from "node:path";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import { BaseCollector } from "./base.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Constants
6
+ // ---------------------------------------------------------------------------
7
+ /** Files to collect as full content (small, valuable config files) */
8
+ const TARGETED_FILES = [
9
+ { path: "CLAUDE.md" }, // user-level instructions
10
+ { path: "settings.json", redact: true }, // settings (contains API tokens)
11
+ { path: "stats-cache.json" }, // aggregate usage stats
12
+ { path: "plugins/installed_plugins.json" }, // plugin inventory
13
+ { path: "plugins/blocklist.json" }, // plugin blocklist
14
+ { path: "history.jsonl", redact: true, maxSize: 2 * 1024 * 1024, slim: true }, // prompt history (excluded in slim mode)
15
+ ];
16
+ /**
17
+ * Collects Claude Code configuration files with targeted, safe collection.
18
+ *
19
+ * What we collect:
20
+ * - ~/CLAUDE.md (user-level instructions)
21
+ * - ~/.claude/CLAUDE.md, settings.json, stats-cache.json
22
+ * - ~/.claude/plugins/installed_plugins.json, plugins/blocklist.json
23
+ * - ~/.claude/history.jsonl
24
+ * - Conversation metadata summaries (title, timestamps, token counts)
25
+ * from projects/sessions-index.json files — NOT full conversation content
26
+ *
27
+ * What we skip:
28
+ * - debug/, telemetry/, transcripts/, cache/, paste-cache/
29
+ * - shell-snapshots/, session-env/, statsig/
30
+ * - All .jsonl session content files
31
+ * - .git/ directories inside plugins
32
+ * - Binary files, large files
33
+ */
34
+ export class ClaudeConfigCollector extends BaseCollector {
35
+ id = "claude-config";
36
+ label = "Claude Code Configuration";
37
+ category = "config";
38
+ slim;
39
+ constructor(homeDir, options) {
40
+ super(homeDir);
41
+ this.slim = options?.slim ?? false;
42
+ }
43
+ async collect() {
44
+ return this.timed(async (result) => {
45
+ const claudeDir = join(this.homeDir, ".claude");
46
+ // 1. Collect ~/CLAUDE.md (home-level instructions)
47
+ const homeMd = await this.safeReadFile(join(this.homeDir, "CLAUDE.md"), result);
48
+ if (homeMd)
49
+ result.files.push(homeMd);
50
+ // 2. Collect targeted config files from ~/.claude/
51
+ for (const { path: relativePath, redact, maxSize, slim: slimExclude } of TARGETED_FILES) {
52
+ // Skip files marked as slim-excluded when in slim mode
53
+ if (this.slim && slimExclude)
54
+ continue;
55
+ const file = await this.safeReadFile(join(claudeDir, relativePath), result, { redact, maxSize });
56
+ if (file)
57
+ result.files.push(file);
58
+ }
59
+ // 3. Collect conversation metadata summaries (skip in slim mode)
60
+ if (!this.slim) {
61
+ const summaryFile = await this.collectSessionSummaries(claudeDir, result);
62
+ if (summaryFile)
63
+ result.files.push(summaryFile);
64
+ }
65
+ });
66
+ }
67
+ /**
68
+ * Iterate over all projects/sessions-index.json files and extract
69
+ * lightweight metadata for each conversation session.
70
+ *
71
+ * Returns a synthetic CollectedFile containing JSON with all project
72
+ * summaries — no actual conversation content is included.
73
+ */
74
+ async collectSessionSummaries(claudeDir, result) {
75
+ const projectsDir = join(claudeDir, "projects");
76
+ const summaries = [];
77
+ try {
78
+ // Each subdirectory under projects/ is a hashed project path
79
+ const projectDirs = await readdir(projectsDir, { withFileTypes: true });
80
+ for (const entry of projectDirs) {
81
+ if (!entry.isDirectory())
82
+ continue;
83
+ const indexPath = join(projectsDir, entry.name, "sessions-index.json");
84
+ try {
85
+ const raw = await readFile(indexPath, "utf-8");
86
+ const index = JSON.parse(raw);
87
+ if (!index.entries?.length)
88
+ continue;
89
+ summaries.push({
90
+ projectPath: index.originalPath ?? entry.name,
91
+ sessions: index.entries.map((e) => ({
92
+ sessionId: e.sessionId,
93
+ firstPrompt: e.firstPrompt,
94
+ messageCount: e.messageCount,
95
+ created: e.created,
96
+ modified: e.modified,
97
+ gitBranch: e.gitBranch,
98
+ projectPath: e.projectPath,
99
+ isSidechain: e.isSidechain,
100
+ })),
101
+ });
102
+ }
103
+ catch {
104
+ // sessions-index.json missing or malformed — skip silently
105
+ }
106
+ }
107
+ }
108
+ catch (err) {
109
+ if (err.code !== "ENOENT") {
110
+ result.errors.push(`Failed to read Claude projects directory: ${err.message}`);
111
+ }
112
+ return null;
113
+ }
114
+ if (summaries.length === 0)
115
+ return null;
116
+ const content = JSON.stringify(summaries, null, 2);
117
+ return {
118
+ path: join(claudeDir, "projects", "__sessions-summary.json"),
119
+ content,
120
+ sizeBytes: Buffer.byteLength(content, "utf-8"),
121
+ };
122
+ }
123
+ }
124
+ //# sourceMappingURL=claude-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-config.js","sourceRoot":"","sources":["../../src/collectors/claude-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { BaseCollector } from "./base.js";
2
+ import type { CollectorCategory, CollectorResult } from "@otter/core";
3
+ /**
4
+ * Collects installed Homebrew packages (formulae + casks).
5
+ * List-only: no binary content is collected.
6
+ */
7
+ export declare class HomebrewCollector extends BaseCollector {
8
+ readonly id = "homebrew";
9
+ readonly label = "Homebrew Packages";
10
+ readonly category: CollectorCategory;
11
+ /** Overridable for testing — executes a shell command and returns stdout */
12
+ _execCommand: (cmd: string) => Promise<string>;
13
+ collect(): Promise<CollectorResult>;
14
+ private listPackages;
15
+ }
16
+ //# sourceMappingURL=homebrew.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"homebrew.d.ts","sourceRoot":"","sources":["../../src/collectors/homebrew.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EAEhB,MAAM,aAAa,CAAC;AAIrB;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,aAAa;IAClD,QAAQ,CAAC,EAAE,cAAc;IACzB,QAAQ,CAAC,KAAK,uBAAuB;IACrC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAiB;IAErD,4EAA4E;IAC5E,YAAY,GAAU,KAAK,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC,CAGjD;IAEI,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC;YAoB3B,YAAY;CAmB3B"}
@@ -0,0 +1,43 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { BaseCollector } from "./base.js";
4
+ const execAsync = promisify(exec);
5
+ /**
6
+ * Collects installed Homebrew packages (formulae + casks).
7
+ * List-only: no binary content is collected.
8
+ */
9
+ export class HomebrewCollector extends BaseCollector {
10
+ id = "homebrew";
11
+ label = "Homebrew Packages";
12
+ category = "environment";
13
+ /** Overridable for testing — executes a shell command and returns stdout */
14
+ _execCommand = async (cmd) => {
15
+ const { stdout } = await execAsync(cmd);
16
+ return stdout;
17
+ };
18
+ async collect() {
19
+ return this.timed(async (result) => {
20
+ // Collect formulae
21
+ const formulae = await this.listPackages("brew list --formula", "formula", result);
22
+ result.lists.push(...formulae);
23
+ // Collect casks
24
+ const casks = await this.listPackages("brew list --cask", "cask", result);
25
+ result.lists.push(...casks);
26
+ });
27
+ }
28
+ async listPackages(cmd, type, result) {
29
+ try {
30
+ const output = await this._execCommand(cmd);
31
+ return output
32
+ .split("\n")
33
+ .map((line) => line.trim())
34
+ .filter((line) => line.length > 0)
35
+ .map((name) => ({ name, meta: { type } }));
36
+ }
37
+ catch (err) {
38
+ result.errors.push(`Failed to run '${cmd}': ${err.message}`);
39
+ return [];
40
+ }
41
+ }
42
+ }
43
+ //# sourceMappingURL=homebrew.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"homebrew.js","sourceRoot":"","sources":["../../src/collectors/homebrew.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAO1C,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;AAElC;;;GAGG;AACH,MAAM,OAAO,iBAAkB,SAAQ,aAAa;IACzC,EAAE,GAAG,UAAU,CAAC;IAChB,KAAK,GAAG,mBAAmB,CAAC;IAC5B,QAAQ,GAAsB,aAAa,CAAC;IAErD,4EAA4E;IAC5E,YAAY,GAAG,KAAK,EAAE,GAAW,EAAmB,EAAE;QACpD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;QACxC,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;IAEF,KAAK,CAAC,OAAO;QACX,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;YACjC,mBAAmB;YACnB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,YAAY,CACtC,qBAAqB,EACrB,SAAS,EACT,MAAM,CACP,CAAC;YACF,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;YAE/B,gBAAgB;YAChB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,CACnC,kBAAkB,EAClB,MAAM,EACN,MAAM,CACP,CAAC;YACF,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,YAAY,CACxB,GAAW,EACX,IAAY,EACZ,MAAuB;QAEvB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;YAC5C,OAAO,MAAM;iBACV,KAAK,CAAC,IAAI,CAAC;iBACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;iBAC1B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;iBACjC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;QAC/C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,MAAM,CAAC,IAAI,CAChB,kBAAkB,GAAG,MAAO,GAAa,CAAC,OAAO,EAAE,CACpD,CAAC;YACF,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,21 @@
1
+ export { BaseCollector } from "./base.js";
2
+ export { ClaudeConfigCollector } from "./claude-config.js";
3
+ export { OpenCodeConfigCollector } from "./opencode-config.js";
4
+ export { ShellConfigCollector } from "./shell-config.js";
5
+ export { HomebrewCollector } from "./homebrew.js";
6
+ export { ApplicationsCollector } from "./applications.js";
7
+ import type { Collector } from "@otter/core";
8
+ /**
9
+ * Options for creating the default set of collectors.
10
+ */
11
+ export interface CollectorOptions {
12
+ /** If true, exclude behavior data (history.jsonl, session summaries) */
13
+ slim?: boolean;
14
+ /** Base URL for deterministic icon URLs (default: s.zhe.to/apps/otter) */
15
+ iconBaseUrl?: string;
16
+ }
17
+ /**
18
+ * Create all default collectors targeting the current system.
19
+ */
20
+ export declare function createDefaultCollectors(homeDir?: string, options?: CollectorOptions): Collector[];
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/collectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAW7C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,wEAAwE;IACxE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,MAAkB,EAC3B,OAAO,GAAE,gBAAqB,GAC7B,SAAS,EAAE,CASb"}
@@ -0,0 +1,28 @@
1
+ export { BaseCollector } from "./base.js";
2
+ export { ClaudeConfigCollector } from "./claude-config.js";
3
+ export { OpenCodeConfigCollector } from "./opencode-config.js";
4
+ export { ShellConfigCollector } from "./shell-config.js";
5
+ export { HomebrewCollector } from "./homebrew.js";
6
+ export { ApplicationsCollector } from "./applications.js";
7
+ import { ClaudeConfigCollector } from "./claude-config.js";
8
+ import { OpenCodeConfigCollector } from "./opencode-config.js";
9
+ import { ShellConfigCollector } from "./shell-config.js";
10
+ import { HomebrewCollector } from "./homebrew.js";
11
+ import { ApplicationsCollector } from "./applications.js";
12
+ import { homedir } from "node:os";
13
+ /** Default R2 public base URL for app icon assets */
14
+ const DEFAULT_ICON_BASE_URL = "https://s.zhe.to/apps/otter";
15
+ /**
16
+ * Create all default collectors targeting the current system.
17
+ */
18
+ export function createDefaultCollectors(homeDir = homedir(), options = {}) {
19
+ const iconBaseUrl = options.iconBaseUrl ?? DEFAULT_ICON_BASE_URL;
20
+ return [
21
+ new ClaudeConfigCollector(homeDir, { slim: options.slim }),
22
+ new OpenCodeConfigCollector(homeDir),
23
+ new ShellConfigCollector(homeDir),
24
+ new HomebrewCollector(homeDir),
25
+ new ApplicationsCollector(homeDir, "/Applications", iconBaseUrl),
26
+ ];
27
+ }
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/collectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAG1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,qDAAqD;AACrD,MAAM,qBAAqB,GAAG,6BAA6B,CAAC;AAY5D;;GAEG;AACH,MAAM,UAAU,uBAAuB,CACrC,UAAkB,OAAO,EAAE,EAC3B,UAA4B,EAAE;IAE9B,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,qBAAqB,CAAC;IACjE,OAAO;QACL,IAAI,qBAAqB,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;QAC1D,IAAI,uBAAuB,CAAC,OAAO,CAAC;QACpC,IAAI,oBAAoB,CAAC,OAAO,CAAC;QACjC,IAAI,iBAAiB,CAAC,OAAO,CAAC;QAC9B,IAAI,qBAAqB,CAAC,OAAO,EAAE,eAAe,EAAE,WAAW,CAAC;KACjE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { BaseCollector } from "./base.js";
2
+ import type { CollectorCategory, CollectorResult } from "@otter/core";
3
+ /**
4
+ * Parse YAML-like frontmatter from a SKILL.md file.
5
+ * Extracts simple `key: value` pairs between `---` delimiters.
6
+ * Exported for testing.
7
+ */
8
+ export declare function parseSkillFrontmatter(content: string): Record<string, string>;
9
+ /**
10
+ * Collects OpenCode configuration files:
11
+ * - ~/.config/opencode/ config files (excluding skills content)
12
+ * - Skill names from ~/.config/opencode/skills/ and ~/.agents/skills/
13
+ */
14
+ export declare class OpenCodeConfigCollector extends BaseCollector {
15
+ readonly id = "opencode-config";
16
+ readonly label = "OpenCode Configuration";
17
+ readonly category: CollectorCategory;
18
+ collect(): Promise<CollectorResult>;
19
+ private collectSkillNames;
20
+ }
21
+ //# sourceMappingURL=opencode-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-config.d.ts","sourceRoot":"","sources":["../../src/collectors/opencode-config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EAEhB,MAAM,aAAa,CAAC;AAQrB;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAaxB;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,aAAa;IACxD,QAAQ,CAAC,EAAE,qBAAqB;IAChC,QAAQ,CAAC,KAAK,4BAA4B;IAC1C,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAY;IAE1C,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC;YAsB3B,iBAAiB;CA2ChC"}
@@ -0,0 +1,88 @@
1
+ import { join } from "node:path";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { BaseCollector } from "./base.js";
4
+ /** Directories that contain skills (list-only, not full content) */
5
+ const SKILLS_DIRS = [
6
+ { relative: ".config/opencode/skills", source: ".config/opencode/skills" },
7
+ { relative: ".agents/skills", source: ".agents/skills" },
8
+ ];
9
+ /**
10
+ * Parse YAML-like frontmatter from a SKILL.md file.
11
+ * Extracts simple `key: value` pairs between `---` delimiters.
12
+ * Exported for testing.
13
+ */
14
+ export function parseSkillFrontmatter(content) {
15
+ const meta = {};
16
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
17
+ if (!match)
18
+ return meta;
19
+ for (const line of match[1].split("\n")) {
20
+ // Match simple key: value (not nested YAML)
21
+ const kv = line.match(/^(\w[\w-]*)\s*:\s*(.+)$/);
22
+ if (kv) {
23
+ meta[kv[1].trim()] = kv[2].trim();
24
+ }
25
+ }
26
+ return meta;
27
+ }
28
+ /**
29
+ * Collects OpenCode configuration files:
30
+ * - ~/.config/opencode/ config files (excluding skills content)
31
+ * - Skill names from ~/.config/opencode/skills/ and ~/.agents/skills/
32
+ */
33
+ export class OpenCodeConfigCollector extends BaseCollector {
34
+ id = "opencode-config";
35
+ label = "OpenCode Configuration";
36
+ category = "config";
37
+ async collect() {
38
+ return this.timed(async (result) => {
39
+ // 1. Collect config files from ~/.config/opencode/ (excluding skills dir)
40
+ const configDir = join(this.homeDir, ".config", "opencode");
41
+ const files = await this.collectDir(configDir, result, {
42
+ filter: (path) => !path.includes("/skills/"),
43
+ redact: true,
44
+ });
45
+ result.files.push(...files);
46
+ // 2. Collect skill names as list items
47
+ for (const skillDir of SKILLS_DIRS) {
48
+ const skills = await this.collectSkillNames(join(this.homeDir, skillDir.relative), skillDir.source, result);
49
+ result.lists.push(...skills);
50
+ }
51
+ });
52
+ }
53
+ async collectSkillNames(dirPath, source, result) {
54
+ const items = [];
55
+ try {
56
+ const entries = await readdir(dirPath, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ if (!entry.isDirectory())
59
+ continue;
60
+ const meta = { source };
61
+ const location = `file://${join(dirPath, entry.name, "SKILL.md")}`;
62
+ meta.location = location;
63
+ // Try to parse SKILL.md frontmatter for description
64
+ try {
65
+ const skillMd = await readFile(join(dirPath, entry.name, "SKILL.md"), "utf-8");
66
+ const fm = parseSkillFrontmatter(skillMd);
67
+ if (fm.description) {
68
+ meta.description = fm.description;
69
+ }
70
+ if (fm.name) {
71
+ meta.skillName = fm.name;
72
+ }
73
+ }
74
+ catch {
75
+ // SKILL.md missing or unreadable — not an error, just skip enrichment
76
+ }
77
+ items.push({ name: entry.name, meta });
78
+ }
79
+ }
80
+ catch (err) {
81
+ if (err.code !== "ENOENT") {
82
+ result.errors.push(`Failed to read skills directory ${dirPath}: ${err.message}`);
83
+ }
84
+ }
85
+ return items;
86
+ }
87
+ }
88
+ //# sourceMappingURL=opencode-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-config.js","sourceRoot":"","sources":["../../src/collectors/opencode-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { BaseCollector } from "./base.js";
2
+ import type { CollectorCategory, CollectorResult } from "@otter/core";
3
+ /**
4
+ * Classify an SSH filename as a key type, or null if not a key.
5
+ * Exported for testing.
6
+ */
7
+ export declare function classifySshFile(name: string): "private-key" | "public-key" | null;
8
+ /**
9
+ * Collects shell and developer environment configuration files:
10
+ * - Common dotfiles (.zshrc, .bashrc, .gitconfig, etc.)
11
+ * - SSH config (but NOT private/public keys — only presence indicators)
12
+ */
13
+ export declare class ShellConfigCollector extends BaseCollector {
14
+ readonly id = "shell-config";
15
+ readonly label = "Shell Configuration";
16
+ readonly category: CollectorCategory;
17
+ collect(): Promise<CollectorResult>;
18
+ /**
19
+ * Scan ~/.ssh/ for key files and return presence indicators.
20
+ * Key content is NEVER read — only filenames and metadata.
21
+ */
22
+ private detectSshKeys;
23
+ }
24
+ //# sourceMappingURL=shell-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-config.d.ts","sourceRoot":"","sources":["../../src/collectors/shell-config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EAEhB,MAAM,aAAa,CAAC;AAgDrB;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,GACX,aAAa,GAAG,YAAY,GAAG,IAAI,CAcrC;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,aAAa;IACrD,QAAQ,CAAC,EAAE,kBAAkB;IAC7B,QAAQ,CAAC,KAAK,yBAAyB;IACvC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAiB;IAE/C,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC;IA2BzC;;;OAGG;YACW,aAAa;CA0C5B"}