@struktur/sdk 2.1.1 → 2.2.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 (105) hide show
  1. package/dist/index.js +4111 -0
  2. package/dist/index.js.map +1 -0
  3. package/dist/parsers.js +492 -0
  4. package/dist/parsers.js.map +1 -0
  5. package/dist/strategies.js +2435 -0
  6. package/dist/strategies.js.map +1 -0
  7. package/package.json +25 -13
  8. package/src/agent-cli-integration.test.ts +0 -47
  9. package/src/agent-export.test.ts +0 -17
  10. package/src/agent-tool-labels.test.ts +0 -50
  11. package/src/artifacts/AGENTS.md +0 -16
  12. package/src/artifacts/fileToArtifact.test.ts +0 -37
  13. package/src/artifacts/fileToArtifact.ts +0 -44
  14. package/src/artifacts/input.test.ts +0 -243
  15. package/src/artifacts/input.ts +0 -360
  16. package/src/artifacts/providers.test.ts +0 -19
  17. package/src/artifacts/providers.ts +0 -7
  18. package/src/artifacts/urlToArtifact.test.ts +0 -23
  19. package/src/artifacts/urlToArtifact.ts +0 -19
  20. package/src/auth/AGENTS.md +0 -11
  21. package/src/auth/config.test.ts +0 -132
  22. package/src/auth/config.ts +0 -186
  23. package/src/auth/tokens.test.ts +0 -58
  24. package/src/auth/tokens.ts +0 -229
  25. package/src/chunking/AGENTS.md +0 -11
  26. package/src/chunking/ArtifactBatcher.test.ts +0 -22
  27. package/src/chunking/ArtifactBatcher.ts +0 -110
  28. package/src/chunking/ArtifactSplitter.test.ts +0 -38
  29. package/src/chunking/ArtifactSplitter.ts +0 -151
  30. package/src/debug/AGENTS.md +0 -79
  31. package/src/debug/logger.test.ts +0 -244
  32. package/src/debug/logger.ts +0 -211
  33. package/src/extract.test.ts +0 -22
  34. package/src/extract.ts +0 -150
  35. package/src/fields.test.ts +0 -681
  36. package/src/fields.ts +0 -246
  37. package/src/index.test.ts +0 -20
  38. package/src/index.ts +0 -110
  39. package/src/llm/AGENTS.md +0 -9
  40. package/src/llm/LLMClient.test.ts +0 -394
  41. package/src/llm/LLMClient.ts +0 -264
  42. package/src/llm/RetryingRunner.test.ts +0 -174
  43. package/src/llm/RetryingRunner.ts +0 -270
  44. package/src/llm/message.test.ts +0 -42
  45. package/src/llm/message.ts +0 -47
  46. package/src/llm/models.test.ts +0 -82
  47. package/src/llm/models.ts +0 -190
  48. package/src/llm/resolveModel.ts +0 -86
  49. package/src/merge/AGENTS.md +0 -6
  50. package/src/merge/Deduplicator.test.ts +0 -108
  51. package/src/merge/Deduplicator.ts +0 -45
  52. package/src/merge/SmartDataMerger.test.ts +0 -177
  53. package/src/merge/SmartDataMerger.ts +0 -56
  54. package/src/parsers/AGENTS.md +0 -58
  55. package/src/parsers/collect.test.ts +0 -56
  56. package/src/parsers/collect.ts +0 -31
  57. package/src/parsers/index.ts +0 -6
  58. package/src/parsers/mime.test.ts +0 -91
  59. package/src/parsers/mime.ts +0 -137
  60. package/src/parsers/npm.ts +0 -26
  61. package/src/parsers/pdf.test.ts +0 -394
  62. package/src/parsers/pdf.ts +0 -194
  63. package/src/parsers/runner.test.ts +0 -95
  64. package/src/parsers/runner.ts +0 -177
  65. package/src/parsers/types.ts +0 -29
  66. package/src/prompts/AGENTS.md +0 -8
  67. package/src/prompts/DeduplicationPrompt.test.ts +0 -41
  68. package/src/prompts/DeduplicationPrompt.ts +0 -37
  69. package/src/prompts/ExtractorPrompt.test.ts +0 -21
  70. package/src/prompts/ExtractorPrompt.ts +0 -72
  71. package/src/prompts/ParallelMergerPrompt.test.ts +0 -8
  72. package/src/prompts/ParallelMergerPrompt.ts +0 -37
  73. package/src/prompts/SequentialExtractorPrompt.test.ts +0 -24
  74. package/src/prompts/SequentialExtractorPrompt.ts +0 -82
  75. package/src/prompts/formatArtifacts.test.ts +0 -39
  76. package/src/prompts/formatArtifacts.ts +0 -46
  77. package/src/strategies/AGENTS.md +0 -6
  78. package/src/strategies/DoublePassAutoMergeStrategy.test.ts +0 -53
  79. package/src/strategies/DoublePassAutoMergeStrategy.ts +0 -410
  80. package/src/strategies/DoublePassStrategy.test.ts +0 -48
  81. package/src/strategies/DoublePassStrategy.ts +0 -266
  82. package/src/strategies/ParallelAutoMergeStrategy.test.ts +0 -152
  83. package/src/strategies/ParallelAutoMergeStrategy.ts +0 -345
  84. package/src/strategies/ParallelStrategy.test.ts +0 -61
  85. package/src/strategies/ParallelStrategy.ts +0 -208
  86. package/src/strategies/SequentialAutoMergeStrategy.test.ts +0 -66
  87. package/src/strategies/SequentialAutoMergeStrategy.ts +0 -325
  88. package/src/strategies/SequentialStrategy.test.ts +0 -53
  89. package/src/strategies/SequentialStrategy.ts +0 -142
  90. package/src/strategies/SimpleStrategy.test.ts +0 -46
  91. package/src/strategies/SimpleStrategy.ts +0 -94
  92. package/src/strategies/concurrency.test.ts +0 -16
  93. package/src/strategies/concurrency.ts +0 -14
  94. package/src/strategies/index.test.ts +0 -20
  95. package/src/strategies/index.ts +0 -7
  96. package/src/strategies/utils.test.ts +0 -76
  97. package/src/strategies/utils.ts +0 -95
  98. package/src/tokenization.test.ts +0 -119
  99. package/src/tokenization.ts +0 -71
  100. package/src/types.test.ts +0 -25
  101. package/src/types.ts +0 -174
  102. package/src/validation/AGENTS.md +0 -7
  103. package/src/validation/validator.test.ts +0 -204
  104. package/src/validation/validator.ts +0 -90
  105. package/tsconfig.json +0 -22
@@ -1,19 +0,0 @@
1
- import type { Artifact } from "../types";
2
-
3
- export const urlToArtifact = async (url: string): Promise<Artifact> => {
4
- const response = await fetch(url);
5
- if (!response.ok) {
6
- throw new Error(`Failed to fetch artifact: ${response.status} ${response.statusText}`);
7
- }
8
-
9
- const data = (await response.json()) as Omit<Artifact, "raw"> & {
10
- raw?: () => Promise<Buffer>;
11
- };
12
-
13
- return {
14
- ...data,
15
- raw:
16
- data.raw ??
17
- (async () => Buffer.from(JSON.stringify(data.contents ?? []))),
18
- };
19
- };
@@ -1,11 +0,0 @@
1
- Auth module
2
-
3
- - Purpose: persist and resolve provider API tokens and CLI defaults.
4
- - Key files: `tokens.ts`, `config.ts`.
5
- - Design: prefers macOS Keychain when available; otherwise uses `~/.config/struktur/tokens.json` with strict permissions.
6
- - Config store (`config.ts`): stores `defaultModel` (string), `aliases` (Record<string, string>), and `parsers` (ParsersConfig) in `~/.config/struktur/config.json`.
7
- - Alias API: `listAliases`, `getAlias`, `setAlias`, `deleteAlias`, `resolveAlias` (resolves alias → model spec, passthrough if not an alias).
8
- - Parsers config API: `listParsers`, `getParser`, `setParser`, `deleteParser`.
9
- - `setParser` validates that `command-file` type parsers contain `FILE_PATH` in the command string.
10
- - Environment variables: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, OPENCODE_API_KEY, OPENROUTER_API_KEY.
11
- - Tests: `tokens.test.ts`.
@@ -1,132 +0,0 @@
1
- import { test, expect } from "bun:test";
2
- import path from "node:path";
3
- import os from "node:os";
4
- import { rm } from "node:fs/promises";
5
- import {
6
- listParsers,
7
- getParser,
8
- setParser,
9
- deleteParser,
10
- } from "./config";
11
-
12
- const makeTempDir = () => {
13
- const suffix = Math.random().toString(16).slice(2);
14
- return path.join(os.tmpdir(), `struktur-test-${suffix}`);
15
- };
16
-
17
- test("listParsers returns empty object when no parsers configured", async () => {
18
- const tempDir = makeTempDir();
19
- process.env.STRUKTUR_CONFIG_DIR = tempDir;
20
-
21
- try {
22
- const parsers = await listParsers();
23
- expect(parsers).toEqual({});
24
- } finally {
25
- delete process.env.STRUKTUR_CONFIG_DIR;
26
- await rm(tempDir, { recursive: true, force: true });
27
- }
28
- });
29
-
30
- test("setParser stores an npm parser", async () => {
31
- const tempDir = makeTempDir();
32
- process.env.STRUKTUR_CONFIG_DIR = tempDir;
33
-
34
- try {
35
- await setParser("application/pdf", { type: "npm", package: "my-pdf-parser" });
36
- const parser = await getParser("application/pdf");
37
- expect(parser).toEqual({ type: "npm", package: "my-pdf-parser" });
38
- } finally {
39
- delete process.env.STRUKTUR_CONFIG_DIR;
40
- await rm(tempDir, { recursive: true, force: true });
41
- }
42
- });
43
-
44
- test("setParser stores a command-file parser", async () => {
45
- const tempDir = makeTempDir();
46
- process.env.STRUKTUR_CONFIG_DIR = tempDir;
47
-
48
- try {
49
- await setParser("application/pdf", {
50
- type: "command-file",
51
- command: "my-cmd FILE_PATH output",
52
- });
53
- const parser = await getParser("application/pdf");
54
- expect(parser).toEqual({ type: "command-file", command: "my-cmd FILE_PATH output" });
55
- } finally {
56
- delete process.env.STRUKTUR_CONFIG_DIR;
57
- await rm(tempDir, { recursive: true, force: true });
58
- }
59
- });
60
-
61
- test("setParser rejects command-file without FILE_PATH placeholder", async () => {
62
- const tempDir = makeTempDir();
63
- process.env.STRUKTUR_CONFIG_DIR = tempDir;
64
-
65
- try {
66
- await expect(
67
- setParser("application/pdf", { type: "command-file", command: "my-cmd --input" })
68
- ).rejects.toThrow("FILE_PATH");
69
- } finally {
70
- delete process.env.STRUKTUR_CONFIG_DIR;
71
- await rm(tempDir, { recursive: true, force: true });
72
- }
73
- });
74
-
75
- test("setParser stores a command-stdin parser", async () => {
76
- const tempDir = makeTempDir();
77
- process.env.STRUKTUR_CONFIG_DIR = tempDir;
78
-
79
- try {
80
- await setParser("text/csv", { type: "command-stdin", command: "csv-to-json" });
81
- const parser = await getParser("text/csv");
82
- expect(parser).toEqual({ type: "command-stdin", command: "csv-to-json" });
83
- } finally {
84
- delete process.env.STRUKTUR_CONFIG_DIR;
85
- await rm(tempDir, { recursive: true, force: true });
86
- }
87
- });
88
-
89
- test("listParsers returns all stored parsers", async () => {
90
- const tempDir = makeTempDir();
91
- process.env.STRUKTUR_CONFIG_DIR = tempDir;
92
-
93
- try {
94
- await setParser("application/pdf", { type: "npm", package: "pdf-parser" });
95
- await setParser("text/csv", { type: "command-stdin", command: "csv-parse" });
96
- const parsers = await listParsers();
97
- expect(parsers["application/pdf"]).toEqual({ type: "npm", package: "pdf-parser" });
98
- expect(parsers["text/csv"]).toEqual({ type: "command-stdin", command: "csv-parse" });
99
- } finally {
100
- delete process.env.STRUKTUR_CONFIG_DIR;
101
- await rm(tempDir, { recursive: true, force: true });
102
- }
103
- });
104
-
105
- test("deleteParser removes stored parser and returns true", async () => {
106
- const tempDir = makeTempDir();
107
- process.env.STRUKTUR_CONFIG_DIR = tempDir;
108
-
109
- try {
110
- await setParser("application/pdf", { type: "npm", package: "pdf-parser" });
111
- const deleted = await deleteParser("application/pdf");
112
- expect(deleted).toBe(true);
113
- const parser = await getParser("application/pdf");
114
- expect(parser).toBeUndefined();
115
- } finally {
116
- delete process.env.STRUKTUR_CONFIG_DIR;
117
- await rm(tempDir, { recursive: true, force: true });
118
- }
119
- });
120
-
121
- test("deleteParser returns false when parser does not exist", async () => {
122
- const tempDir = makeTempDir();
123
- process.env.STRUKTUR_CONFIG_DIR = tempDir;
124
-
125
- try {
126
- const deleted = await deleteParser("application/pdf");
127
- expect(deleted).toBe(false);
128
- } finally {
129
- delete process.env.STRUKTUR_CONFIG_DIR;
130
- await rm(tempDir, { recursive: true, force: true });
131
- }
132
- });
@@ -1,186 +0,0 @@
1
- import path from "node:path";
2
- import os from "node:os";
3
- import { chmod, mkdir } from "node:fs/promises";
4
- import type { ParserDef, ParsersConfig } from "@struktur/sdk";
5
-
6
- type TelemetryConfig = {
7
- enabled: boolean;
8
- provider: string;
9
- url?: string;
10
- apiKey?: string;
11
- projectName?: string;
12
- publicKey?: string; // For Langfuse
13
- secretKey?: string; // For Langfuse
14
- baseUrl?: string; // For Langfuse
15
- sampleRate?: number;
16
- };
17
-
18
- type ConfigStore = {
19
- version: 1;
20
- defaultModel?: string;
21
- aliases?: Record<string, string>;
22
- parsers?: ParsersConfig;
23
- telemetry?: TelemetryConfig;
24
- };
25
-
26
- const CONFIG_DIR_ENV = "STRUKTUR_CONFIG_DIR";
27
-
28
- const resolveConfigDir = () => {
29
- return process.env[CONFIG_DIR_ENV] ?? path.join(os.homedir(), ".config", "struktur");
30
- };
31
-
32
- const resolveConfigPath = () => path.join(resolveConfigDir(), "config.json");
33
-
34
- const emptyStore = (): ConfigStore => ({ version: 1 });
35
-
36
- const readConfigStore = async (): Promise<ConfigStore> => {
37
- const configPath = resolveConfigPath();
38
- const exists = await Bun.file(configPath).exists();
39
- if (!exists) {
40
- return emptyStore();
41
- }
42
- const raw = await Bun.file(configPath).text();
43
- const parsed = JSON.parse(raw) as ConfigStore;
44
- if (!parsed || parsed.version !== 1) {
45
- return emptyStore();
46
- }
47
- return parsed;
48
- };
49
-
50
- const writeConfigStore = async (store: ConfigStore) => {
51
- const configDir = resolveConfigDir();
52
- const configPath = resolveConfigPath();
53
- await mkdir(configDir, { recursive: true, mode: 0o700 });
54
- await Bun.write(configPath, JSON.stringify(store, null, 2));
55
- await chmod(configDir, 0o700);
56
- await chmod(configPath, 0o600);
57
- };
58
-
59
- export const getDefaultModel = async () => {
60
- const store = await readConfigStore();
61
- return store.defaultModel;
62
- };
63
-
64
- export const setDefaultModel = async (model: string) => {
65
- const store = await readConfigStore();
66
- store.defaultModel = model;
67
- await writeConfigStore(store);
68
- return model;
69
- };
70
-
71
- // --- Alias management ---
72
-
73
- export const listAliases = async (): Promise<Record<string, string>> => {
74
- const store = await readConfigStore();
75
- return store.aliases ?? {};
76
- };
77
-
78
- export const getAlias = async (alias: string): Promise<string | undefined> => {
79
- const store = await readConfigStore();
80
- return store.aliases?.[alias];
81
- };
82
-
83
- export const setAlias = async (alias: string, model: string): Promise<string> => {
84
- const store = await readConfigStore();
85
- store.aliases ??= {};
86
- store.aliases[alias] = model;
87
- await writeConfigStore(store);
88
- return model;
89
- };
90
-
91
- export const deleteAlias = async (alias: string): Promise<boolean> => {
92
- const store = await readConfigStore();
93
- if (!store.aliases?.[alias]) {
94
- return false;
95
- }
96
- delete store.aliases[alias];
97
- await writeConfigStore(store);
98
- return true;
99
- };
100
-
101
- /**
102
- * Resolve a model spec: if it matches a stored alias, return the aliased model string.
103
- * Otherwise return the original spec unchanged.
104
- */
105
- export const resolveAlias = async (modelSpec: string): Promise<string> => {
106
- const aliases = await listAliases();
107
- return aliases[modelSpec] ?? modelSpec;
108
- };
109
-
110
- // --- Parser config management ---
111
-
112
- export const listParsers = async (): Promise<ParsersConfig> => {
113
- const store = await readConfigStore();
114
- return store.parsers ?? {};
115
- };
116
-
117
- export const getParser = async (mimeType: string): Promise<ParserDef | undefined> => {
118
- const store = await readConfigStore();
119
- return store.parsers?.[mimeType];
120
- };
121
-
122
- export const setParser = async (mimeType: string, def: ParserDef): Promise<void> => {
123
- if (def.type === "command-file" && !def.command.includes("FILE_PATH")) {
124
- throw new Error(
125
- `command-file parser must contain FILE_PATH placeholder in the command string. Got: "${def.command}"`
126
- );
127
- }
128
- const store = await readConfigStore();
129
- store.parsers ??= {};
130
- store.parsers[mimeType] = def;
131
- await writeConfigStore(store);
132
- };
133
-
134
- export const deleteParser = async (mimeType: string): Promise<boolean> => {
135
- const store = await readConfigStore();
136
- if (!store.parsers?.[mimeType]) {
137
- return false;
138
- }
139
- delete store.parsers[mimeType];
140
- await writeConfigStore(store);
141
- return true;
142
- };
143
-
144
- // --- Telemetry config management ---
145
-
146
- export const getTelemetryConfig = async (): Promise<TelemetryConfig | undefined> => {
147
- const store = await readConfigStore();
148
- return store.telemetry;
149
- };
150
-
151
- export const setTelemetryConfig = async (config: TelemetryConfig): Promise<void> => {
152
- const store = await readConfigStore();
153
- store.telemetry = config;
154
- await writeConfigStore(store);
155
- };
156
-
157
- export const enableTelemetry = async (
158
- provider: string,
159
- options: Omit<TelemetryConfig, "enabled" | "provider">
160
- ): Promise<void> => {
161
- const store = await readConfigStore();
162
- store.telemetry = {
163
- enabled: true,
164
- provider,
165
- ...options,
166
- };
167
- await writeConfigStore(store);
168
- };
169
-
170
- export const disableTelemetry = async (): Promise<void> => {
171
- const store = await readConfigStore();
172
- if (store.telemetry) {
173
- store.telemetry.enabled = false;
174
- }
175
- await writeConfigStore(store);
176
- };
177
-
178
- export const deleteTelemetryConfig = async (): Promise<boolean> => {
179
- const store = await readConfigStore();
180
- if (!store.telemetry) {
181
- return false;
182
- }
183
- delete store.telemetry;
184
- await writeConfigStore(store);
185
- return true;
186
- };
@@ -1,58 +0,0 @@
1
- import { test, expect } from "bun:test";
2
- import path from "node:path";
3
- import os from "node:os";
4
- import { rm } from "node:fs/promises";
5
- import {
6
- deleteProviderToken,
7
- getProviderTokenOrThrow,
8
- listStoredProviders,
9
- resolveProviderToken,
10
- setProviderToken,
11
- } from "./tokens";
12
-
13
- const makeTempDir = () => {
14
- const suffix = Math.random().toString(16).slice(2);
15
- return path.join(os.tmpdir(), `struktur-test-${suffix}`);
16
- };
17
-
18
- test("setProviderToken stores token in file when keychain disabled", async () => {
19
- const tempDir = makeTempDir();
20
- process.env.STRUKTUR_CONFIG_DIR = tempDir;
21
- process.env.STRUKTUR_DISABLE_KEYCHAIN = "1";
22
-
23
- try {
24
- const storage = await setProviderToken("openai", "sk-test", "auto");
25
- expect(storage).toBe("file");
26
-
27
- const resolved = await resolveProviderToken("openai");
28
- expect(resolved).toBe("sk-test");
29
-
30
- const listed = await listStoredProviders();
31
- expect(listed).toEqual([{ provider: "openai", storage: "file" }]);
32
-
33
- const token = await getProviderTokenOrThrow("openai");
34
- expect(token).toBe("sk-test");
35
- } finally {
36
- delete process.env.STRUKTUR_CONFIG_DIR;
37
- delete process.env.STRUKTUR_DISABLE_KEYCHAIN;
38
- await rm(tempDir, { recursive: true, force: true });
39
- }
40
- });
41
-
42
- test("deleteProviderToken removes stored token", async () => {
43
- const tempDir = makeTempDir();
44
- process.env.STRUKTUR_CONFIG_DIR = tempDir;
45
- process.env.STRUKTUR_DISABLE_KEYCHAIN = "1";
46
-
47
- try {
48
- await setProviderToken("anthropic", "sk-test", "auto");
49
- const deleted = await deleteProviderToken("anthropic");
50
- expect(deleted).toBe(true);
51
- const resolved = await resolveProviderToken("anthropic");
52
- expect(resolved).toBeUndefined();
53
- } finally {
54
- delete process.env.STRUKTUR_CONFIG_DIR;
55
- delete process.env.STRUKTUR_DISABLE_KEYCHAIN;
56
- await rm(tempDir, { recursive: true, force: true });
57
- }
58
- });
@@ -1,229 +0,0 @@
1
- import path from "node:path";
2
- import os from "node:os";
3
- import { chmod, mkdir } from "node:fs/promises";
4
-
5
- export type TokenStorageType = "auto" | "keychain" | "file";
6
-
7
- export type TokenEntry = {
8
- storage: "keychain" | "file";
9
- token?: string;
10
- account?: string;
11
- service?: string;
12
- };
13
-
14
- type TokenStore = {
15
- version: 1;
16
- providers: Record<string, TokenEntry>;
17
- };
18
-
19
- const CONFIG_DIR_ENV = "STRUKTUR_CONFIG_DIR";
20
- const DISABLE_KEYCHAIN_ENV = "STRUKTUR_DISABLE_KEYCHAIN";
21
- const SERVICE_ENV = "STRUKTUR_KEYCHAIN_SERVICE";
22
- const DEFAULT_SERVICE = "struktur";
23
-
24
- const resolveConfigDir = () => {
25
- return process.env[CONFIG_DIR_ENV] ?? path.join(os.homedir(), ".config", "struktur");
26
- };
27
-
28
- const resolveTokensPath = () => path.join(resolveConfigDir(), "tokens.json");
29
-
30
- const emptyStore = (): TokenStore => ({ version: 1, providers: {} });
31
-
32
- const readTokenStore = async (): Promise<TokenStore> => {
33
- const tokensPath = resolveTokensPath();
34
- const exists = await Bun.file(tokensPath).exists();
35
- if (!exists) {
36
- return emptyStore();
37
- }
38
- const raw = await Bun.file(tokensPath).text();
39
- const parsed = JSON.parse(raw) as TokenStore;
40
- if (!parsed || parsed.version !== 1 || typeof parsed.providers !== "object") {
41
- return emptyStore();
42
- }
43
- return parsed;
44
- };
45
-
46
- const writeTokenStore = async (store: TokenStore) => {
47
- const configDir = resolveConfigDir();
48
- const tokensPath = resolveTokensPath();
49
- await mkdir(configDir, { recursive: true, mode: 0o700 });
50
- await Bun.write(tokensPath, JSON.stringify(store, null, 2));
51
- await chmod(configDir, 0o700);
52
- await chmod(tokensPath, 0o600);
53
- };
54
-
55
- const isKeychainAvailable = async () => {
56
- if (process.env[DISABLE_KEYCHAIN_ENV]) {
57
- return false;
58
- }
59
- if (process.platform !== "darwin") {
60
- return false;
61
- }
62
- return await Bun.file("/usr/bin/security").exists();
63
- };
64
-
65
- const keychainService = () => process.env[SERVICE_ENV] ?? DEFAULT_SERVICE;
66
-
67
- const runSecurity = async (args: string[]) => {
68
- const proc = Bun.spawn({
69
- cmd: ["/usr/bin/security", ...args],
70
- stdout: "pipe",
71
- stderr: "pipe",
72
- });
73
- const stdout = await new Response(proc.stdout).text();
74
- const stderr = await new Response(proc.stderr).text();
75
- const exitCode = await proc.exited;
76
- if (exitCode !== 0) {
77
- const message = stderr.trim() || `security exited with ${exitCode}`;
78
- throw new Error(message);
79
- }
80
- return stdout;
81
- };
82
-
83
- const writeKeychainToken = async (provider: string, token: string) => {
84
- await runSecurity([
85
- "add-generic-password",
86
- "-a",
87
- provider,
88
- "-s",
89
- keychainService(),
90
- "-w",
91
- token,
92
- "-U",
93
- ]);
94
- };
95
-
96
- const readKeychainToken = async (provider: string) => {
97
- const output = await runSecurity([
98
- "find-generic-password",
99
- "-a",
100
- provider,
101
- "-s",
102
- keychainService(),
103
- "-w",
104
- ]);
105
- return output.trim();
106
- };
107
-
108
- const deleteKeychainToken = async (provider: string) => {
109
- await runSecurity([
110
- "delete-generic-password",
111
- "-a",
112
- provider,
113
- "-s",
114
- keychainService(),
115
- ]);
116
- };
117
-
118
- export const listStoredProviders = async () => {
119
- const store = await readTokenStore();
120
- return Object.entries(store.providers).map(([provider, entry]) => ({
121
- provider,
122
- storage: entry.storage,
123
- }));
124
- };
125
-
126
- export const setProviderToken = async (
127
- provider: string,
128
- token: string,
129
- storage: TokenStorageType = "auto"
130
- ) => {
131
- const store = await readTokenStore();
132
- let resolvedStorage: TokenEntry["storage"] = "file";
133
-
134
- if (storage === "keychain") {
135
- if (!(await isKeychainAvailable())) {
136
- throw new Error("Keychain is not available on this platform.");
137
- }
138
- resolvedStorage = "keychain";
139
- } else if (storage === "auto") {
140
- resolvedStorage = (await isKeychainAvailable()) ? "keychain" : "file";
141
- }
142
-
143
- if (resolvedStorage === "keychain") {
144
- await writeKeychainToken(provider, token);
145
- store.providers[provider] = {
146
- storage: "keychain",
147
- account: provider,
148
- service: keychainService(),
149
- };
150
- } else {
151
- store.providers[provider] = {
152
- storage: "file",
153
- token,
154
- };
155
- }
156
-
157
- await writeTokenStore(store);
158
- return resolvedStorage;
159
- };
160
-
161
- export const deleteProviderToken = async (provider: string) => {
162
- const store = await readTokenStore();
163
- const entry = store.providers[provider];
164
- if (!entry) {
165
- return false;
166
- }
167
-
168
- if (entry.storage === "keychain") {
169
- try {
170
- await deleteKeychainToken(provider);
171
- } catch {
172
- // ignore errors for missing keychain items
173
- }
174
- }
175
-
176
- delete store.providers[provider];
177
- await writeTokenStore(store);
178
- return true;
179
- };
180
-
181
- export const resolveProviderToken = async (provider: string) => {
182
- const store = await readTokenStore();
183
- const entry = store.providers[provider];
184
- if (!entry) {
185
- return undefined;
186
- }
187
-
188
- if (entry.storage === "file") {
189
- return entry.token;
190
- }
191
-
192
- try {
193
- return await readKeychainToken(provider);
194
- } catch {
195
- return undefined;
196
- }
197
- };
198
-
199
- export const getProviderTokenOrThrow = async (provider: string) => {
200
- const token = await resolveProviderToken(provider);
201
- if (!token) {
202
- throw new Error(`No token stored for provider: ${provider}`);
203
- }
204
- return token;
205
- };
206
-
207
- export const resolveProviderEnvVar = (provider: string) => {
208
- switch (provider) {
209
- case "openai":
210
- return "OPENAI_API_KEY";
211
- case "anthropic":
212
- return "ANTHROPIC_API_KEY";
213
- case "google":
214
- return "GOOGLE_GENERATIVE_AI_API_KEY";
215
- case "opencode":
216
- return "OPENCODE_API_KEY";
217
- case "openrouter":
218
- return "OPENROUTER_API_KEY";
219
- default:
220
- return undefined;
221
- }
222
- };
223
-
224
- export const maskToken = (token: string) => {
225
- if (token.length <= 8) {
226
- return "********";
227
- }
228
- return `${token.slice(0, 4)}...${token.slice(-4)}`;
229
- };
@@ -1,11 +0,0 @@
1
- Chunking module
2
-
3
- - Purpose: split and batch artifacts based on token and image limits.
4
- - Key files: `ArtifactSplitter.ts`, `ArtifactBatcher.ts`.
5
- - Design: split large artifact contents into parts, then batch parts to fit limits.
6
- - Tests: `ArtifactSplitter.test.ts`, `ArtifactBatcher.test.ts`.
7
-
8
- IMPORTANT: When modifying chunking/batching logic, you MUST also update the client-side
9
- JavaScript implementation in `src/cli.ts` (`generateArtifactViewerHtml`) to keep the
10
- artifact viewer's chunking visualization in sync. The viewer includes a version stamp
11
- to help users verify they're comparing the correct algorithm version.
@@ -1,22 +0,0 @@
1
- import { test, expect } from "bun:test";
2
- import type { Artifact } from "../types";
3
- import { batchArtifacts } from "./ArtifactBatcher";
4
-
5
- const makeArtifact = (id: string, text: string): Artifact => ({
6
- id,
7
- type: "text",
8
- raw: async () => Buffer.from(text),
9
- contents: [{ text }],
10
- });
11
-
12
- test("batchArtifacts respects maxTokens", () => {
13
- const artifacts = [
14
- makeArtifact("a1", "abcdefgh"),
15
- makeArtifact("a2", "abcdefgh"),
16
- ];
17
-
18
- const batches = batchArtifacts(artifacts, { maxTokens: 2 });
19
- expect(batches.length).toBe(2);
20
- expect(batches[0]?.length).toBe(1);
21
- expect(batches[1]?.length).toBe(1);
22
- });