@theokit/sdk-tools 0.12.0 → 0.14.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.
package/dist/index.cjs CHANGED
@@ -8,6 +8,7 @@ var fs = require('fs');
8
8
  var pathSafety = require('@theokit/sdk/path-safety');
9
9
  var persistence = require('@theokit/sdk/persistence');
10
10
  var child_process = require('child_process');
11
+ var interactive = require('@theokit/sdk/interactive');
11
12
  var promises$1 = require('dns/promises');
12
13
  var net = require('net');
13
14
  var filesystem = require('@theokit/sdk/filesystem');
@@ -265,6 +266,90 @@ function createSessionArtifactStore(options) {
265
266
  }
266
267
  return { write, read, has, list, path };
267
268
  }
269
+
270
+ // src/internal/context-match.ts
271
+ var ContextMatchError = class extends Error {
272
+ reason;
273
+ constructor(reason, message) {
274
+ super(message);
275
+ this.name = "ContextMatchError";
276
+ this.reason = reason;
277
+ }
278
+ };
279
+ function normalizeUnicode(s) {
280
+ return s.replace(/[‐-―−]/g, "-").replace(/[‘-‛]/g, "'").replace(/[“-‟]/g, '"').replace(/[ -    ]/g, " ");
281
+ }
282
+ var LINE_MATCH_LADDER = [
283
+ (s) => s,
284
+ // exact
285
+ (s) => s.replace(/\s+$/, ""),
286
+ // rstrip (ignore trailing whitespace)
287
+ (s) => s.trim(),
288
+ // trim (ignore leading + trailing)
289
+ (s) => normalizeUnicode(s).trim()
290
+ // unicode + trim (loosest)
291
+ ];
292
+ function matchesAt(lines, pattern, i, norm) {
293
+ for (let p = 0; p < pattern.length; p++) {
294
+ const lineAt = lines[i + p];
295
+ const patAt = pattern[p];
296
+ if (lineAt === void 0 || patAt === void 0 || norm(lineAt) !== norm(patAt)) return false;
297
+ }
298
+ return true;
299
+ }
300
+ function findHits(lines, pattern, norm) {
301
+ const hits = [];
302
+ for (let i = 0; i + pattern.length <= lines.length; i++) {
303
+ if (matchesAt(lines, pattern, i, norm)) hits.push(i);
304
+ }
305
+ return hits;
306
+ }
307
+ function seekUniqueLineMatch(lines, pattern, find) {
308
+ if (pattern.length === 0 || pattern.length > lines.length) return null;
309
+ for (const norm of LINE_MATCH_LADDER) {
310
+ const hits = findHits(lines, pattern, norm);
311
+ if (hits.length === 1) return hits[0] ?? null;
312
+ if (hits.length > 1) {
313
+ throw new ContextMatchError(
314
+ "ambiguous",
315
+ `target text is ambiguous (multiple matches); include more context:
316
+ ${find}`
317
+ );
318
+ }
319
+ }
320
+ return null;
321
+ }
322
+ function replaceUnique(content, find, replace) {
323
+ if (find === "") {
324
+ throw new ContextMatchError(
325
+ "empty",
326
+ "empty find is not a valid target; provide the text to replace"
327
+ );
328
+ }
329
+ const first = content.indexOf(find);
330
+ if (first !== -1) {
331
+ if (content.indexOf(find, first + 1) !== -1) {
332
+ throw new ContextMatchError(
333
+ "ambiguous",
334
+ `target text is ambiguous (multiple matches); include more context:
335
+ ${find}`
336
+ );
337
+ }
338
+ return content.slice(0, first) + replace + content.slice(first + find.length);
339
+ }
340
+ const lines = content.split("\n");
341
+ const at = seekUniqueLineMatch(lines, find.split("\n"), find);
342
+ if (at === null) {
343
+ throw new ContextMatchError("not_found", `target text not found:
344
+ ${find}`);
345
+ }
346
+ const patternLen = find.split("\n").length;
347
+ const matchedCrlf = patternLen > 0 && lines.slice(at, at + patternLen).every((l) => l.endsWith("\r"));
348
+ const replaceLines = matchedCrlf ? replace.split("\n").map((l) => l.endsWith("\r") ? l : `${l}\r`) : replace.split("\n");
349
+ return [...lines.slice(0, at), ...replaceLines, ...lines.slice(at + patternLen)].join("\n");
350
+ }
351
+
352
+ // src/edit-file.ts
268
353
  function createEditFileTool(opts) {
269
354
  const { projectRoot } = opts;
270
355
  return sdk.Tool.create({
@@ -306,26 +391,36 @@ function createEditFileTool(opts) {
306
391
  const exactIdx = content.indexOf(old_string);
307
392
  if (exactIdx !== -1) {
308
393
  await promises.copyFile(absolutePath, `${absolutePath}.bak`);
309
- const result2 = content.slice(0, exactIdx) + new_string + content.slice(exactIdx + old_string.length);
310
- await promises.writeFile(absolutePath, result2, "utf-8");
394
+ const result = content.slice(0, exactIdx) + new_string + content.slice(exactIdx + old_string.length);
395
+ await promises.writeFile(absolutePath, result, "utf-8");
311
396
  return JSON.stringify({ ok: true, replacements: 1 });
312
397
  }
313
398
  const normalizedContent = normalizeWhitespace(content);
314
399
  const normalizedOld = normalizeWhitespace(old_string);
315
400
  const normalizedIdx = normalizedContent.indexOf(normalizedOld);
316
- if (normalizedIdx === -1) {
317
- return JSON.stringify({ ok: false, error: "no_match", path });
401
+ if (normalizedIdx !== -1) {
402
+ const span = findOriginalSpan(
403
+ content,
404
+ normalizedContent,
405
+ normalizedIdx,
406
+ normalizedOld.length
407
+ );
408
+ await promises.copyFile(absolutePath, `${absolutePath}.bak`);
409
+ const result = content.slice(0, span.start) + new_string + content.slice(span.end);
410
+ await promises.writeFile(absolutePath, result, "utf-8");
411
+ return JSON.stringify({ ok: true, replacements: 1 });
412
+ }
413
+ try {
414
+ const result = replaceUnique(content, old_string, new_string);
415
+ await promises.copyFile(absolutePath, `${absolutePath}.bak`);
416
+ await promises.writeFile(absolutePath, result, "utf-8");
417
+ return JSON.stringify({ ok: true, replacements: 1 });
418
+ } catch (err) {
419
+ if (err instanceof ContextMatchError) {
420
+ return JSON.stringify({ ok: false, error: "no_match", path });
421
+ }
422
+ throw err;
318
423
  }
319
- const span = findOriginalSpan(
320
- content,
321
- normalizedContent,
322
- normalizedIdx,
323
- normalizedOld.length
324
- );
325
- await promises.copyFile(absolutePath, `${absolutePath}.bak`);
326
- const result = content.slice(0, span.start) + new_string + content.slice(span.end);
327
- await promises.writeFile(absolutePath, result, "utf-8");
328
- return JSON.stringify({ ok: true, replacements: 1 });
329
424
  }
330
425
  });
331
426
  }
@@ -597,6 +692,60 @@ function globToRegex(pattern) {
597
692
  }
598
693
  return new RegExp(`^${regexStr}$`);
599
694
  }
695
+ function toErrorJson(err) {
696
+ if (err instanceof interactive.InteractiveUnavailableError) {
697
+ return JSON.stringify({ ok: false, error: "interactive_unavailable" });
698
+ }
699
+ if (err instanceof interactive.NoSuchSessionError) {
700
+ return JSON.stringify({ ok: false, error: "no_such_session" });
701
+ }
702
+ throw err;
703
+ }
704
+ function createInteractiveShellTool(opts) {
705
+ const { interactive: interactive$1 } = opts;
706
+ return sdk.Tool.create({
707
+ name: "interactive_shell",
708
+ description: "Start an interactive shell session for a command that PROMPTS for input or is a REPL (python, node, `git rebase -i`, a `read` prompt) \u2014 NOT for one-shot commands (use shell_exec). Returns a session_id; drive it with write_stdin, reading the incremental output each step. Returns { ok, session_id, output } or { ok: false, error }.",
709
+ inputSchema: zod.z.object({
710
+ command: zod.z.string().min(1).describe("Command to run interactively, e.g. 'python3' or 'bash -i'."),
711
+ yield_time_ms: zod.z.number().int().positive().optional().describe("How long to wait for startup output before returning (clamped by the backend).")
712
+ }),
713
+ handler: async ({ command, yield_time_ms }, ctx) => {
714
+ try {
715
+ const backend = await interactive.resolveInteractive(interactive$1, ctx ?? {});
716
+ const { sessionId, output } = await backend.startInteractive(command, {
717
+ yieldMs: yield_time_ms
718
+ });
719
+ return JSON.stringify({ ok: true, session_id: sessionId, output });
720
+ } catch (err) {
721
+ return toErrorJson(err);
722
+ }
723
+ }
724
+ });
725
+ }
726
+ function createWriteStdinTool(opts) {
727
+ const { interactive: interactive$1 } = opts;
728
+ return sdk.Tool.create({
729
+ name: "write_stdin",
730
+ description: "Write input to a live interactive session (from interactive_shell) and read the output it produces during the wait window. Include a trailing newline to submit a line. Returns { ok, output, alive } (alive:false means the session exited) or { ok: false, error }.",
731
+ inputSchema: zod.z.object({
732
+ session_id: zod.z.string().min(1).describe("The session_id returned by interactive_shell."),
733
+ input: zod.z.string().describe("Text to write to stdin (add a trailing '\\n' to submit a line)."),
734
+ yield_time_ms: zod.z.number().int().positive().optional().describe("How long to wait for output before returning (clamped by the backend).")
735
+ }),
736
+ handler: async ({ session_id, input, yield_time_ms }, ctx) => {
737
+ try {
738
+ const backend = await interactive.resolveInteractive(interactive$1, ctx ?? {});
739
+ const { output, alive } = await backend.writeStdin(session_id, input, {
740
+ yieldMs: yield_time_ms
741
+ });
742
+ return JSON.stringify({ ok: true, output, alive });
743
+ } catch (err) {
744
+ return toErrorJson(err);
745
+ }
746
+ }
747
+ });
748
+ }
600
749
  var CatastrophicCommandError = class extends sdk.ConfigurationError {
601
750
  name = "CatastrophicCommandError";
602
751
  constructor(reason) {
@@ -2250,6 +2399,7 @@ async function isBinaryFile(absolutePath) {
2250
2399
  }
2251
2400
 
2252
2401
  exports.CatastrophicCommandError = CatastrophicCommandError;
2402
+ exports.ContextMatchError = ContextMatchError;
2253
2403
  exports.DEFAULT_TOOL_GUIDANCE = DEFAULT_TOOL_GUIDANCE;
2254
2404
  exports.ReadTracker = ReadTracker;
2255
2405
  exports.ReasoningTools = ReasoningTools;
@@ -2265,6 +2415,7 @@ exports.createEditFileTool = createEditFileTool;
2265
2415
  exports.createGenericHttpSearchAdapter = createGenericHttpSearchAdapter;
2266
2416
  exports.createGitDiffTool = createGitDiffTool;
2267
2417
  exports.createGlobTool = createGlobTool;
2418
+ exports.createInteractiveShellTool = createInteractiveShellTool;
2268
2419
  exports.createListDirTool = createListDirTool;
2269
2420
  exports.createPlanModeTool = createPlanModeTool;
2270
2421
  exports.createQuestionTool = createQuestionTool;
@@ -2277,6 +2428,7 @@ exports.createTodolistTool = createTodolistTool;
2277
2428
  exports.createWebFetchTool = createWebFetchTool;
2278
2429
  exports.createWebSearchTool = createWebSearchTool;
2279
2430
  exports.createWriteFileTool = createWriteFileTool;
2431
+ exports.createWriteStdinTool = createWriteStdinTool;
2280
2432
  exports.denyCatastrophicCommands = denyCatastrophicCommands;
2281
2433
  exports.formatCode = formatCode;
2282
2434
  exports.formatDiff = formatDiff;
@@ -2286,6 +2438,7 @@ exports.injectGuidance = injectGuidance;
2286
2438
  exports.isBlockedIp = isBlockedIp;
2287
2439
  exports.isCommandAllowed = isCommandAllowed;
2288
2440
  exports.renderToolList = renderToolList;
2441
+ exports.replaceUnique = replaceUnique;
2289
2442
  exports.resolveAndScreen = resolveAndScreen;
2290
2443
  exports.screenedFetch = screenedFetch;
2291
2444
  exports.todoItemsToPlanNodes = todoItemsToPlanNodes;