@theokit/sdk-tools 0.27.5 → 0.28.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.
@@ -0,0 +1,241 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { readFile, mkdir, stat, writeFile, rename, rm } from 'fs/promises';
3
+ import { posix, resolve, relative, sep, dirname, extname, basename } from 'path';
4
+ import { Tool } from '@theokit/sdk';
5
+ import { z } from 'zod';
6
+ import { safePathJoin, assertNoSymlinkEscape, PathTraversalError, ForbiddenPathError } from '@theokit/sdk/path-safety';
7
+
8
+ // src/screenshot-logbook.ts
9
+
10
+ // src/path-scope.ts
11
+ function checkPathScope(path, projectRoot) {
12
+ if (path === void 0 || path === "") return null;
13
+ try {
14
+ const abs = safePathJoin(projectRoot, path);
15
+ assertNoSymlinkEscape(abs, projectRoot);
16
+ return null;
17
+ } catch (err) {
18
+ if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
19
+ return JSON.stringify({ ok: false, error: "path_traversal", path });
20
+ }
21
+ throw err;
22
+ }
23
+ }
24
+ var SENSITIVE_SEGMENTS = /* @__PURE__ */ new Set([".env", ".git", "node_modules", ".theo"]);
25
+ function isForbiddenAtAnyDepth(path) {
26
+ const segs = path.replace(/\\/g, "/").split("/").filter(Boolean);
27
+ return segs.some((s) => {
28
+ if (s === ".env.example") return false;
29
+ return SENSITIVE_SEGMENTS.has(s) || /^\.env\./.test(s);
30
+ });
31
+ }
32
+
33
+ // src/screenshot-logbook.ts
34
+ var MEDIA_TYPES = /* @__PURE__ */ new Map([
35
+ [".png", "image/png"],
36
+ [".jpg", "image/jpeg"],
37
+ [".jpeg", "image/jpeg"],
38
+ [".gif", "image/gif"],
39
+ [".webp", "image/webp"]
40
+ ]);
41
+ var DEFAULT_MAX_CAPTION_CHARS = 500;
42
+ var DEFAULT_MAX_ENTRIES = 1e3;
43
+ var DEFAULT_LOG_PATH = "logbook/screenshots.jsonl";
44
+ var DEFAULT_LIST_LIMIT = 20;
45
+ var entrySchema = z.object({
46
+ id: z.number(),
47
+ path: z.string(),
48
+ caption: z.string(),
49
+ media_type: z.string(),
50
+ bytes: z.number(),
51
+ recorded_at: z.string()
52
+ });
53
+ var inputSchema = z.object({
54
+ action: z.enum(["record", "list"]).describe("'record' adds an entry; 'list' reads the log."),
55
+ path: z.string().optional().describe("record: the image path, relative to the project root."),
56
+ caption: z.string().optional().describe("record: what the image shows."),
57
+ limit: z.number().int().positive().optional().describe(`list: how many of the latest entries to return. Default ${DEFAULT_LIST_LIMIT}.`)
58
+ });
59
+ var json = (result) => JSON.stringify(result);
60
+ var LOG_PATH_REFUSAL = { ok: false, error: "path_traversal", target: "log_path" };
61
+ var errorCode = (err) => err.code ?? "unknown";
62
+ var isFailure = (value) => "ok" in value;
63
+ function requirePositiveInteger(name, value) {
64
+ if (!Number.isInteger(value) || value <= 0) {
65
+ throw new RangeError(
66
+ `createScreenshotLogbookTool: ${name} must be a positive integer, got ${value}`
67
+ );
68
+ }
69
+ return value;
70
+ }
71
+ function refuseLogPath(config) {
72
+ if (checkPathScope(config.logPath, config.projectRoot) !== null) return LOG_PATH_REFUSAL;
73
+ return isForbiddenAtAnyDepth(rootRelative(config.logPath, config.projectRoot)) ? LOG_PATH_REFUSAL : void 0;
74
+ }
75
+ function rootRelative(path, projectRoot) {
76
+ return relative(resolve(projectRoot), safePathJoin(projectRoot, path)).split(sep).join("/");
77
+ }
78
+ function confineImage(path, projectRoot) {
79
+ const refusal = { ok: false, error: "path_traversal", path };
80
+ if (checkPathScope(path, projectRoot) !== null) return refusal;
81
+ const relativePath = rootRelative(path, projectRoot);
82
+ return isForbiddenAtAnyDepth(relativePath) ? refusal : relativePath;
83
+ }
84
+ async function inspectImage(relativePath, projectRoot) {
85
+ const mediaType = MEDIA_TYPES.get(extname(relativePath).toLowerCase());
86
+ if (mediaType === void 0) {
87
+ const supported = [...MEDIA_TYPES.keys()];
88
+ return { ok: false, error: "unsupported_image_type", path: relativePath, supported };
89
+ }
90
+ const info = await stat(safePathJoin(projectRoot, relativePath)).catch(() => void 0);
91
+ if (info === void 0 || !info.isFile()) {
92
+ return { ok: false, error: "image_not_found", path: relativePath };
93
+ }
94
+ if (info.size === 0) return { ok: false, error: "empty_image", path: relativePath };
95
+ return { media_type: mediaType, bytes: info.size };
96
+ }
97
+ async function acceptRecord(input, config) {
98
+ const { path, caption } = input;
99
+ if (!path) return { ok: false, error: "invalid_input", message: "record needs a path" };
100
+ if (!caption?.trim())
101
+ return { ok: false, error: "invalid_input", message: "record needs a caption" };
102
+ if (caption.length > config.maxCaptionChars) {
103
+ return {
104
+ ok: false,
105
+ error: "caption_too_long",
106
+ chars: caption.length,
107
+ max_chars: config.maxCaptionChars
108
+ };
109
+ }
110
+ const relativePath = confineImage(path, config.projectRoot);
111
+ if (typeof relativePath !== "string") return relativePath;
112
+ const refusedLog = refuseLogPath(config);
113
+ if (refusedLog !== void 0) return refusedLog;
114
+ const image = await inspectImage(relativePath, config.projectRoot);
115
+ if (isFailure(image)) return image;
116
+ return { path: relativePath, caption, ...image };
117
+ }
118
+ function parseLog(text) {
119
+ const entries = [];
120
+ const lines = text.split("\n");
121
+ for (let index = 0; index < lines.length; index += 1) {
122
+ const line = lines[index] ?? "";
123
+ if (line.trim() === "") continue;
124
+ const entry = parseEntry(line);
125
+ if (entry === void 0) return { ok: false, error: "log_corrupt", line: index + 1 };
126
+ entries.push(entry);
127
+ }
128
+ return { entries, text };
129
+ }
130
+ function parseEntry(line) {
131
+ let value;
132
+ try {
133
+ value = JSON.parse(line);
134
+ } catch {
135
+ return void 0;
136
+ }
137
+ const parsed = entrySchema.safeParse(value);
138
+ return parsed.success ? parsed.data : void 0;
139
+ }
140
+ async function readLog(logFile) {
141
+ let text;
142
+ try {
143
+ text = await readFile(logFile, "utf8");
144
+ } catch (err) {
145
+ const code = errorCode(err);
146
+ if (code === "ENOENT") return { entries: [], text: "" };
147
+ return { ok: false, error: "write_failed", message: code };
148
+ }
149
+ return parseLog(text);
150
+ }
151
+ async function writeAtomic(target, text) {
152
+ const temp = `${dirname(target)}/.${basename(target)}.${randomBytes(6).toString("hex")}.tmp`;
153
+ try {
154
+ await writeFile(temp, text, { flag: "wx", mode: 384 });
155
+ await rename(temp, target);
156
+ } catch (err) {
157
+ await rm(temp, { force: true });
158
+ throw err;
159
+ }
160
+ }
161
+ async function appendEntry(config, log, entry) {
162
+ try {
163
+ await mkdir(dirname(config.logFile), { recursive: true });
164
+ const refused = refuseLogPath(config);
165
+ if (refused !== void 0) return refused;
166
+ const separator = log.text === "" || log.text.endsWith("\n") ? "" : "\n";
167
+ await writeAtomic(config.logFile, `${log.text}${separator}${JSON.stringify(entry)}
168
+ `);
169
+ return void 0;
170
+ } catch (err) {
171
+ return { ok: false, error: "write_failed", message: errorCode(err) };
172
+ }
173
+ }
174
+ async function record(input, config) {
175
+ try {
176
+ return await recordOrThrow(input, config);
177
+ } catch (err) {
178
+ return { ok: false, error: "write_failed", message: errorCode(err) };
179
+ }
180
+ }
181
+ async function recordOrThrow(input, config) {
182
+ const image = await acceptRecord(input, config);
183
+ if (isFailure(image)) return image;
184
+ const log = await readLog(config.logFile);
185
+ if (isFailure(log)) return log;
186
+ if (log.entries.length >= config.maxEntries) {
187
+ return { ok: false, error: "log_full", max_entries: config.maxEntries };
188
+ }
189
+ const entry = {
190
+ id: log.entries.length + 1,
191
+ ...image,
192
+ recorded_at: config.now().toISOString()
193
+ };
194
+ const failed = await appendEntry(config, log, entry);
195
+ return failed ?? { ok: true, entry, total: log.entries.length + 1 };
196
+ }
197
+ async function list(limit, config) {
198
+ if (limit !== void 0 && !(Number.isInteger(limit) && limit > 0)) {
199
+ return { ok: false, error: "invalid_input", message: "limit must be a positive integer" };
200
+ }
201
+ const refused = refuseLogPath(config);
202
+ if (refused !== void 0) return refused;
203
+ const log = await readLog(config.logFile);
204
+ if (isFailure(log)) return log;
205
+ const count = Math.min(limit ?? DEFAULT_LIST_LIMIT, config.maxEntries);
206
+ return { ok: true, entries: log.entries.slice(-count), total: log.entries.length };
207
+ }
208
+ function resolveConfig(opts) {
209
+ const logPath = posix.normalize((opts.logPath ?? DEFAULT_LOG_PATH).replace(/\\/g, "/"));
210
+ return {
211
+ projectRoot: opts.projectRoot,
212
+ logPath,
213
+ logFile: resolve(opts.projectRoot, logPath),
214
+ now: opts.now ?? (() => /* @__PURE__ */ new Date()),
215
+ maxCaptionChars: requirePositiveInteger(
216
+ "maxCaptionChars",
217
+ opts.maxCaptionChars ?? DEFAULT_MAX_CAPTION_CHARS
218
+ ),
219
+ maxEntries: requirePositiveInteger("maxEntries", opts.maxEntries ?? DEFAULT_MAX_ENTRIES)
220
+ };
221
+ }
222
+ function createScreenshotLogbookTool(opts) {
223
+ const config = resolveConfig(opts);
224
+ let queue = Promise.resolve();
225
+ return Tool.create({
226
+ name: opts.name ?? "screenshot_logbook",
227
+ description: opts.description ?? "Keep a captioned log of screenshots and other images in the project. action 'record' adds an image that already exists (path relative to the project root, png/jpeg/gif/webp) with a caption; action 'list' returns the most recent entries. Returns JSON with { ok, entry, total } or { ok, entries, total }, or { ok: false, error }. Use view_image to look at a listed path.",
228
+ inputSchema,
229
+ handler: async (input) => {
230
+ if (input.action === "list") return json(await list(input.limit, config));
231
+ const run = () => record(input, config);
232
+ const next = queue.then(run, run);
233
+ queue = next;
234
+ return json(await next);
235
+ }
236
+ });
237
+ }
238
+
239
+ export { DEFAULT_MAX_CAPTION_CHARS, DEFAULT_MAX_ENTRIES, createScreenshotLogbookTool };
240
+ //# sourceMappingURL=screenshot-logbook.js.map
241
+ //# sourceMappingURL=screenshot-logbook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/path-scope.ts","../src/screenshot-logbook.ts"],"names":[],"mappings":"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","file":"screenshot-logbook.js","sourcesContent":["/**\n * Shared `safePathJoin + assertNoSymlinkEscape` helper for built-in coding\n * tools (git-diff, run-vitest, list-dir, read-file, search-text). Returns a\n * pre-formatted `path_traversal` JSON string on failure or `null` on pass.\n *\n * @internal\n */\n\nimport {\n assertNoSymlinkEscape,\n ForbiddenPathError,\n PathTraversalError,\n safePathJoin,\n} from \"./internal/path-guard.js\";\n\nexport function checkPathScope(path: string | undefined, projectRoot: string): string | null {\n if (path === undefined || path === \"\") return null;\n try {\n const abs = safePathJoin(projectRoot, path);\n assertNoSymlinkEscape(abs, projectRoot);\n return null;\n } catch (err) {\n if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {\n return JSON.stringify({ ok: false, error: \"path_traversal\", path });\n }\n throw err;\n }\n}\n\n/**\n * Segments that may never appear in a path honored by `allowAbsolute`.\n *\n * M76 — promoted from `read-file.ts`, where it was private. Duplicating it in `list-dir` would duplicate\n * security KNOWLEDGE: the copies would have to agree on what counts as a secret, and one fixed\n * without the other reopens the hole in the forgotten tool.\n */\nconst SENSITIVE_SEGMENTS = new Set([\".env\", \".git\", \"node_modules\", \".theo\"]);\n\n/**\n * ANY-segment secret guard — the half of `allowAbsolute` that cannot be separated.\n *\n * `isForbiddenPath` only blocks the sensitive item when it is the FIRST segment (relative to the\n * project). An absolute path (`/home/u/proj/.env/sub`) puts it deeper, and it would pass. This one\n * checks every segment, closing the \"reads-anywhere\" exfiltration.\n */\nexport function isForbiddenAtAnyDepth(path: string): boolean {\n const segs = path.replace(/\\\\/g, \"/\").split(\"/\").filter(Boolean);\n return segs.some((s) => {\n if (s === \".env.example\") return false; // a template — safe\n return SENSITIVE_SEGMENTS.has(s) || /^\\.env\\./.test(s);\n });\n}\n","/**\n * `screenshot_logbook` — a captioned log of images that already exist inside the project root.\n *\n * `record` appends a reference entry (root-relative path, media type, size, caption, time) to a\n * JSON Lines log under `projectRoot`; `list` reads it back. The image is never copied and its bytes\n * are never read, so a `list` stays small in context. `view_image` shows a listed image.\n *\n * Return shape (always a JSON string):\n * - `record`: `{ ok: true, entry, total }`\n * - `list`: `{ ok: true, entries, total }` (the last `limit` entries, oldest first)\n * - `{ ok: false, error }` with `error` one of `invalid_input`, `path_traversal`,\n * `caption_too_long`, `unsupported_image_type`, `image_not_found`, `empty_image`, `log_full`,\n * `log_corrupt` (with the 1-based `line`), `write_failed` (the errno code only)\n *\n * No failure changes the log. A `record` rewrites the file through a temp file and a rename, and\n * `record` calls on one instance run one at a time, so concurrent calls in one turn each land once.\n *\n * Limits, accepted and stated:\n * - Two processes (or two tool instances) writing one log are not serialised and can lose an entry.\n * - The log is a journal of looks, not a set: recording the same image twice gives two entries.\n * - Ids are positions (`entry count + 1`), not keys: a hand-edited log can repeat an id.\n * - A trailing carriage return on a line is tolerated; a byte-order mark makes line 1 `log_corrupt`.\n * - A symlink swapped in by another process between the confinement check and the rename is not\n * caught (the same-user race the sibling tools leave open).\n * - `write_failed` carries only the errno code, since a file-system message names host paths.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { mkdir, readFile, rename, rm, stat, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, extname, posix, relative, resolve, sep } from \"node:path\";\n\nimport type { CustomTool } from \"@theokit/sdk\";\nimport { Tool } from \"@theokit/sdk\";\nimport { z } from \"zod\";\n\nimport { safePathJoin } from \"./internal/path-guard.js\";\nimport { checkPathScope, isForbiddenAtAnyDepth } from \"./path-scope.js\";\n\n/** The image extensions `view_image` can show, mapped to their media type. A closed table, as there. */\nconst MEDIA_TYPES = new Map<string, string>([\n [\".png\", \"image/png\"],\n [\".jpg\", \"image/jpeg\"],\n [\".jpeg\", \"image/jpeg\"],\n [\".gif\", \"image/gif\"],\n [\".webp\", \"image/webp\"],\n]);\n\n/** Default ceiling on a caption, in UTF-16 code units. A default, not a measured limit. */\nexport const DEFAULT_MAX_CAPTION_CHARS = 500;\n\n/** Default ceiling on the entries one log holds; it bounds the cost of each rewrite. A default. */\nexport const DEFAULT_MAX_ENTRIES = 1000;\n\nconst DEFAULT_LOG_PATH = \"logbook/screenshots.jsonl\";\nconst DEFAULT_LIST_LIMIT = 20;\n\nconst entrySchema = z.object({\n id: z.number(),\n path: z.string(),\n caption: z.string(),\n media_type: z.string(),\n bytes: z.number(),\n recorded_at: z.string(),\n});\n\n/** One line of the log. `path` is root-relative with forward slashes; `recorded_at` is ISO 8601. */\nexport type LogbookEntry = z.infer<typeof entrySchema>;\n\n/** Options for {@link createScreenshotLogbookTool}. Only `projectRoot` is required. */\nexport interface CreateScreenshotLogbookToolOptions {\n /** Root every path is resolved inside. */\n projectRoot: string;\n /** The log file, relative to `projectRoot`. Omitted ⇒ `logbook/screenshots.jsonl`. Its directory is created when absent. */\n logPath?: string;\n /** Clock for `recorded_at`. Omitted ⇒ the system clock. */\n now?: () => Date;\n /** Name exposed to the model. Omitted ⇒ `screenshot_logbook`. */\n name?: string;\n /** Description exposed to the model. Omitted ⇒ the literal below. */\n description?: string;\n /** Longest caption. Omitted ⇒ {@link DEFAULT_MAX_CAPTION_CHARS}. A caption of exactly this length fits. */\n maxCaptionChars?: number;\n /** Most entries one log holds. Omitted ⇒ {@link DEFAULT_MAX_ENTRIES}. A record past it is `log_full`. */\n maxEntries?: number;\n}\n\ntype LogbookFailure =\n | { ok: false; error: \"invalid_input\"; message: string }\n | { ok: false; error: \"path_traversal\"; path: string }\n | { ok: false; error: \"path_traversal\"; target: \"log_path\" }\n | { ok: false; error: \"caption_too_long\"; chars: number; max_chars: number }\n | { ok: false; error: \"unsupported_image_type\"; path: string; supported: string[] }\n | { ok: false; error: \"image_not_found\"; path: string }\n | { ok: false; error: \"empty_image\"; path: string }\n | { ok: false; error: \"log_full\"; max_entries: number }\n | { ok: false; error: \"log_corrupt\"; line: number }\n | { ok: false; error: \"write_failed\"; message: string };\n\n/** The tool's result, before it is serialised to the JSON string the handler returns. */\nexport type ScreenshotLogbookResult =\n | { ok: true; entry: LogbookEntry; total: number }\n | { ok: true; entries: LogbookEntry[]; total: number }\n | LogbookFailure;\n\nconst inputSchema = z.object({\n action: z.enum([\"record\", \"list\"]).describe(\"'record' adds an entry; 'list' reads the log.\"),\n path: z.string().optional().describe(\"record: the image path, relative to the project root.\"),\n caption: z.string().optional().describe(\"record: what the image shows.\"),\n limit: z\n .number()\n .int()\n .positive()\n .optional()\n .describe(`list: how many of the latest entries to return. Default ${DEFAULT_LIST_LIMIT}.`),\n});\n\ntype LogbookInput = z.infer<typeof inputSchema>;\n\n/** What one `record` call needs once its input is accepted. */\ntype ImageToRecord = Omit<LogbookEntry, \"id\" | \"recorded_at\">;\n\n/** The log as read: its parsed entries and its exact text, so a rewrite keeps every existing byte. */\ninterface LogContents {\n entries: LogbookEntry[];\n text: string;\n}\n\ninterface LogbookConfig {\n projectRoot: string;\n logPath: string;\n logFile: string;\n now: () => Date;\n maxCaptionChars: number;\n maxEntries: number;\n}\n\nconst json = (result: ScreenshotLogbookResult): string => JSON.stringify(result);\n\nconst LOG_PATH_REFUSAL: LogbookFailure = { ok: false, error: \"path_traversal\", target: \"log_path\" };\n\n/** The errno code of a file-system failure: its message would hand the model the host's absolute paths. */\nconst errorCode = (err: unknown): string => (err as { code?: string }).code ?? \"unknown\";\n\nconst isFailure = (value: object): value is LogbookFailure => \"ok\" in value;\n\n/** A ceiling of zero, a negative, a fraction or NaN would build a tool that refuses everything or nothing. */\nfunction requirePositiveInteger(name: string, value: number): number {\n if (!Number.isInteger(value) || value <= 0) {\n throw new RangeError(\n `createScreenshotLogbookTool: ${name} must be a positive integer, got ${value}`,\n );\n }\n return value;\n}\n\n/**\n * The log path's refusal, or undefined when the tool may use it. Its own refusal, without the\n * path: the log path is the consumer's configuration, not something the model chose.\n */\nfunction refuseLogPath(config: LogbookConfig): LogbookFailure | undefined {\n if (checkPathScope(config.logPath, config.projectRoot) !== null) return LOG_PATH_REFUSAL;\n return isForbiddenAtAnyDepth(rootRelative(config.logPath, config.projectRoot))\n ? LOG_PATH_REFUSAL\n : undefined;\n}\n\n/**\n * A path already accepted by `checkPathScope`, relative to the root with forward slashes. The secret\n * guard reads this form, so the directories above the root (an absolute path's ancestors) never trip it.\n */\nfunction rootRelative(path: string, projectRoot: string): string {\n return relative(resolve(projectRoot), safePathJoin(projectRoot, path)).split(sep).join(\"/\");\n}\n\n/** The image's root-relative posix path, or the refusal. The secret guard reads the relative path,\n * so directories above the root never trip it. */\nfunction confineImage(path: string, projectRoot: string): string | LogbookFailure {\n const refusal: LogbookFailure = { ok: false, error: \"path_traversal\", path };\n if (checkPathScope(path, projectRoot) !== null) return refusal;\n const relativePath = rootRelative(path, projectRoot);\n return isForbiddenAtAnyDepth(relativePath) ? refusal : relativePath;\n}\n\n/** The media type and size of an image that exists as a non-empty regular file, or the failure. */\nasync function inspectImage(\n relativePath: string,\n projectRoot: string,\n): Promise<{ media_type: string; bytes: number } | LogbookFailure> {\n const mediaType = MEDIA_TYPES.get(extname(relativePath).toLowerCase());\n if (mediaType === undefined) {\n const supported = [...MEDIA_TYPES.keys()];\n return { ok: false, error: \"unsupported_image_type\", path: relativePath, supported };\n }\n const info = await stat(safePathJoin(projectRoot, relativePath)).catch(() => undefined);\n if (info === undefined || !info.isFile()) {\n return { ok: false, error: \"image_not_found\", path: relativePath };\n }\n if (info.size === 0) return { ok: false, error: \"empty_image\", path: relativePath };\n return { media_type: mediaType, bytes: info.size };\n}\n\n/** Checks a `record` input in the documented order, up to (not including) the log. */\nasync function acceptRecord(\n input: LogbookInput,\n config: LogbookConfig,\n): Promise<ImageToRecord | LogbookFailure> {\n const { path, caption } = input;\n if (!path) return { ok: false, error: \"invalid_input\", message: \"record needs a path\" };\n if (!caption?.trim())\n return { ok: false, error: \"invalid_input\", message: \"record needs a caption\" };\n if (caption.length > config.maxCaptionChars) {\n return {\n ok: false,\n error: \"caption_too_long\",\n chars: caption.length,\n max_chars: config.maxCaptionChars,\n };\n }\n const relativePath = confineImage(path, config.projectRoot);\n if (typeof relativePath !== \"string\") return relativePath;\n const refusedLog = refuseLogPath(config);\n if (refusedLog !== undefined) return refusedLog;\n const image = await inspectImage(relativePath, config.projectRoot);\n if (isFailure(image)) return image;\n return { path: relativePath, caption, ...image };\n}\n\n/** Parses the log text line by line; the first line that is not an entry is `log_corrupt`. */\nfunction parseLog(text: string): LogContents | LogbookFailure {\n const entries: LogbookEntry[] = [];\n const lines = text.split(\"\\n\");\n for (let index = 0; index < lines.length; index += 1) {\n const line = lines[index] ?? \"\";\n if (line.trim() === \"\") continue;\n const entry = parseEntry(line);\n if (entry === undefined) return { ok: false, error: \"log_corrupt\", line: index + 1 };\n entries.push(entry);\n }\n return { entries, text };\n}\n\nfunction parseEntry(line: string): LogbookEntry | undefined {\n let value: unknown;\n try {\n value = JSON.parse(line);\n } catch {\n return undefined;\n }\n const parsed = entrySchema.safeParse(value);\n return parsed.success ? parsed.data : undefined;\n}\n\n/** A missing log is an empty one; any other read error is `write_failed` with its errno code. */\nasync function readLog(logFile: string): Promise<LogContents | LogbookFailure> {\n let text: string;\n try {\n text = await readFile(logFile, \"utf8\");\n } catch (err) {\n const code = errorCode(err);\n if (code === \"ENOENT\") return { entries: [], text: \"\" };\n return { ok: false, error: \"write_failed\", message: code };\n }\n return parseLog(text);\n}\n\n/**\n * Writes the text through a temp file in the target's own directory and a rename, so a reader\n * never sees a partial log. A failure removes the temp file and rethrows.\n *\n * Local rather than `replaceFileAtomic`, which does not create its directory (as in `text_to_speech`).\n */\nasync function writeAtomic(target: string, text: string): Promise<void> {\n const temp = `${dirname(target)}/.${basename(target)}.${randomBytes(6).toString(\"hex\")}.tmp`;\n try {\n await writeFile(temp, text, { flag: \"wx\", mode: 0o600 });\n await rename(temp, target);\n } catch (err) {\n await rm(temp, { force: true });\n throw err;\n }\n}\n\n/** Creates the log directory, re-checks it for a symlink escape, then rewrites the log with the new line. */\nasync function appendEntry(\n config: LogbookConfig,\n log: LogContents,\n entry: LogbookEntry,\n): Promise<LogbookFailure | undefined> {\n try {\n await mkdir(dirname(config.logFile), { recursive: true });\n // Checked again now that the directory exists: it narrows, and does not close, the window in\n // which another process could swap a symlink in (see the limits above).\n const refused = refuseLogPath(config);\n if (refused !== undefined) return refused;\n const separator = log.text === \"\" || log.text.endsWith(\"\\n\") ? \"\" : \"\\n\";\n await writeAtomic(config.logFile, `${log.text}${separator}${JSON.stringify(entry)}\\n`);\n return undefined;\n } catch (err) {\n return { ok: false, error: \"write_failed\", message: errorCode(err) };\n }\n}\n\n/**\n * Never rejects: an unexpected throw (a consumer clock that throws, a guard's file-system error) is\n * `write_failed` with its errno code, so the caller always gets a typed result and the instance queue\n * keeps running.\n */\nasync function record(\n input: LogbookInput,\n config: LogbookConfig,\n): Promise<ScreenshotLogbookResult> {\n try {\n return await recordOrThrow(input, config);\n } catch (err) {\n return { ok: false, error: \"write_failed\", message: errorCode(err) };\n }\n}\n\nasync function recordOrThrow(\n input: LogbookInput,\n config: LogbookConfig,\n): Promise<ScreenshotLogbookResult> {\n const image = await acceptRecord(input, config);\n if (isFailure(image)) return image;\n const log = await readLog(config.logFile);\n if (isFailure(log)) return log;\n if (log.entries.length >= config.maxEntries) {\n return { ok: false, error: \"log_full\", max_entries: config.maxEntries };\n }\n const entry: LogbookEntry = {\n id: log.entries.length + 1,\n ...image,\n recorded_at: config.now().toISOString(),\n };\n const failed = await appendEntry(config, log, entry);\n return failed ?? { ok: true, entry, total: log.entries.length + 1 };\n}\n\nasync function list(\n limit: number | undefined,\n config: LogbookConfig,\n): Promise<ScreenshotLogbookResult> {\n if (limit !== undefined && !(Number.isInteger(limit) && limit > 0)) {\n return { ok: false, error: \"invalid_input\", message: \"limit must be a positive integer\" };\n }\n const refused = refuseLogPath(config);\n if (refused !== undefined) return refused;\n const log = await readLog(config.logFile);\n if (isFailure(log)) return log;\n const count = Math.min(limit ?? DEFAULT_LIST_LIMIT, config.maxEntries);\n return { ok: true, entries: log.entries.slice(-count), total: log.entries.length };\n}\n\nfunction resolveConfig(opts: CreateScreenshotLogbookToolOptions): LogbookConfig {\n const logPath = posix.normalize((opts.logPath ?? DEFAULT_LOG_PATH).replace(/\\\\/g, \"/\"));\n return {\n projectRoot: opts.projectRoot,\n logPath,\n logFile: resolve(opts.projectRoot, logPath),\n now: opts.now ?? (() => new Date()),\n maxCaptionChars: requirePositiveInteger(\n \"maxCaptionChars\",\n opts.maxCaptionChars ?? DEFAULT_MAX_CAPTION_CHARS,\n ),\n maxEntries: requirePositiveInteger(\"maxEntries\", opts.maxEntries ?? DEFAULT_MAX_ENTRIES),\n };\n}\n\n/**\n * Build the `screenshot_logbook` tool over a log under `projectRoot`.\n *\n * Order of checks for `record`: the required fields, the caption length, the image path, the log\n * path, the media table, the file itself, then the log (corrupt, full) and the write. `list` checks\n * the log path, then reads.\n */\nexport function createScreenshotLogbookTool(opts: CreateScreenshotLogbookToolOptions): CustomTool {\n const config = resolveConfig(opts);\n // One `record` at a time per instance: two in one turn would both read the same log and one entry\n // would be lost. `record` never rejects, so a failed call never poisons the chain.\n let queue: Promise<unknown> = Promise.resolve();\n\n return Tool.create({\n name: opts.name ?? \"screenshot_logbook\",\n description:\n opts.description ??\n \"Keep a captioned log of screenshots and other images in the project. action 'record' adds \" +\n \"an image that already exists (path relative to the project root, png/jpeg/gif/webp) with a \" +\n \"caption; action 'list' returns the most recent entries. Returns JSON with { ok, entry, total } \" +\n \"or { ok, entries, total }, or { ok: false, error }. Use view_image to look at a listed path.\",\n inputSchema,\n handler: async (input: LogbookInput): Promise<string> => {\n if (input.action === \"list\") return json(await list(input.limit, config));\n const run = () => record(input, config);\n const next = queue.then(run, run);\n queue = next;\n return json(await next);\n },\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theokit/sdk-tools",
3
- "version": "0.27.5",
3
+ "version": "0.28.0",
4
4
  "description": "Built-in tools for @theokit/sdk agents — file system, git, subprocess, search-text, vitest runner.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/usetheokit/theokit-sdk#readme",
@@ -29,6 +29,46 @@
29
29
  "default": "./dist/index.cjs"
30
30
  }
31
31
  },
32
+ "./extract-document": {
33
+ "import": {
34
+ "types": "./dist/extract-document.d.ts",
35
+ "default": "./dist/extract-document.js"
36
+ },
37
+ "require": {
38
+ "types": "./dist/extract-document.d.cts",
39
+ "default": "./dist/extract-document.cjs"
40
+ }
41
+ },
42
+ "./read-pdf": {
43
+ "import": {
44
+ "types": "./dist/read-pdf.d.ts",
45
+ "default": "./dist/read-pdf.js"
46
+ },
47
+ "require": {
48
+ "types": "./dist/read-pdf.d.cts",
49
+ "default": "./dist/read-pdf.cjs"
50
+ }
51
+ },
52
+ "./github-link-preview": {
53
+ "import": {
54
+ "types": "./dist/github-link-preview.d.ts",
55
+ "default": "./dist/github-link-preview.js"
56
+ },
57
+ "require": {
58
+ "types": "./dist/github-link-preview.d.cts",
59
+ "default": "./dist/github-link-preview.cjs"
60
+ }
61
+ },
62
+ "./screenshot-logbook": {
63
+ "import": {
64
+ "types": "./dist/screenshot-logbook.d.ts",
65
+ "default": "./dist/screenshot-logbook.js"
66
+ },
67
+ "require": {
68
+ "types": "./dist/screenshot-logbook.d.cts",
69
+ "default": "./dist/screenshot-logbook.cjs"
70
+ }
71
+ },
32
72
  "./package.json": "./package.json"
33
73
  },
34
74
  "files": [
@@ -39,14 +79,22 @@
39
79
  ],
40
80
  "peerDependencies": {
41
81
  "@theokit/sdk": ">=5.0.0-next.1",
42
- "simple-git": "^3.0.0",
82
+ "mammoth": "^1.13.0",
83
+ "simple-git": "^4.0.1",
84
+ "unpdf": "^2.1.0",
43
85
  "vitest": ">=2.0.0",
44
86
  "zod": "^4.0.0"
45
87
  },
46
88
  "peerDependenciesMeta": {
89
+ "mammoth": {
90
+ "optional": true
91
+ },
47
92
  "simple-git": {
48
93
  "optional": true
49
94
  },
95
+ "unpdf": {
96
+ "optional": true
97
+ },
50
98
  "vitest": {
51
99
  "optional": true
52
100
  }
@@ -55,10 +103,12 @@
55
103
  "tsup": "^8.3.5",
56
104
  "typescript": "^5.7.2",
57
105
  "vitest": "^4.1.8",
106
+ "mammoth": "^1.13.0",
107
+ "unpdf": "^2.1.0",
58
108
  "zod": "^4.0.0",
59
- "@theokit/sdk-cache": "1.0.2",
60
- "@theokit/sdk-handoff": "0.1.5",
61
- "@theokit/sdk": "5.6.0"
109
+ "@theokit/sdk": "5.13.0",
110
+ "@theokit/sdk-handoff": "0.1.6",
111
+ "@theokit/sdk-cache": "1.0.2"
62
112
  },
63
113
  "publishConfig": {
64
114
  "provenance": true,