@tyvm/knowhow 0.0.118 → 0.0.119

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 (123) hide show
  1. package/package.json +1 -3
  2. package/src/agents/base/base.ts +72 -9
  3. package/src/agents/researcher/researcher.ts +9 -2
  4. package/src/agents/tools/list.ts +13 -2
  5. package/src/agents/tools/patch.ts +318 -32
  6. package/src/agents/tools/readFile.ts +48 -5
  7. package/src/chat/modules/AgentModule.ts +12 -0
  8. package/src/cli.ts +2 -0
  9. package/src/clients/anthropic.ts +12 -2
  10. package/src/clients/contextLimits.ts +77 -0
  11. package/src/commands/convert.ts +291 -0
  12. package/src/conversion.ts +15 -61
  13. package/src/index.ts +3 -0
  14. package/src/processors/TokenCompressor.ts +95 -9
  15. package/src/services/AgentSyncFs.ts +26 -4
  16. package/src/services/AgentSyncKnowhowWeb.ts +26 -4
  17. package/src/services/SyncedAgentWatcher.ts +8 -0
  18. package/src/services/conversion/ConversionService.ts +763 -0
  19. package/src/services/conversion/index.ts +2 -0
  20. package/src/services/conversion/types.ts +79 -0
  21. package/src/services/index.ts +8 -1
  22. package/src/services/modules/types.ts +2 -0
  23. package/src/services/watchers/FsSyncer.ts +6 -0
  24. package/src/services/watchers/RemoteSyncer.ts +5 -0
  25. package/tests/agents/tools/readFile.test.ts +88 -0
  26. package/tests/clients/AIClient.test.ts +5 -0
  27. package/tests/clients/contextLimits.test.ts +71 -0
  28. package/tests/patching/patchFileOutput.test.ts +217 -0
  29. package/tests/patching/regression-2026.test.ts +278 -0
  30. package/tests/processors/CustomVariables.test.ts +4 -4
  31. package/tests/processors/TokenCompressor.test.ts +59 -1
  32. package/tests/processors/tools/grepToolResponse.test.ts +72 -0
  33. package/tests/services/ConversionService.test.ts +154 -0
  34. package/tests/test.spec.ts +1 -1
  35. package/tests/unit/clients/AIClient.test.ts +8 -0
  36. package/ts_build/package.json +1 -3
  37. package/ts_build/src/agents/base/base.d.ts +3 -0
  38. package/ts_build/src/agents/base/base.js +46 -3
  39. package/ts_build/src/agents/base/base.js.map +1 -1
  40. package/ts_build/src/agents/researcher/researcher.js +5 -2
  41. package/ts_build/src/agents/researcher/researcher.js.map +1 -1
  42. package/ts_build/src/agents/tools/list.js +10 -2
  43. package/ts_build/src/agents/tools/list.js.map +1 -1
  44. package/ts_build/src/agents/tools/patch.js +202 -24
  45. package/ts_build/src/agents/tools/patch.js.map +1 -1
  46. package/ts_build/src/agents/tools/readFile.d.ts +1 -1
  47. package/ts_build/src/agents/tools/readFile.js +17 -4
  48. package/ts_build/src/agents/tools/readFile.js.map +1 -1
  49. package/ts_build/src/chat/modules/AgentModule.js +12 -0
  50. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  51. package/ts_build/src/cli.js +2 -0
  52. package/ts_build/src/cli.js.map +1 -1
  53. package/ts_build/src/clients/anthropic.js +7 -2
  54. package/ts_build/src/clients/anthropic.js.map +1 -1
  55. package/ts_build/src/clients/contextLimits.js +70 -0
  56. package/ts_build/src/clients/contextLimits.js.map +1 -1
  57. package/ts_build/src/commands/convert.d.ts +2 -0
  58. package/ts_build/src/commands/convert.js +275 -0
  59. package/ts_build/src/commands/convert.js.map +1 -0
  60. package/ts_build/src/conversion.js +6 -38
  61. package/ts_build/src/conversion.js.map +1 -1
  62. package/ts_build/src/index.d.ts +2 -0
  63. package/ts_build/src/index.js +4 -1
  64. package/ts_build/src/index.js.map +1 -1
  65. package/ts_build/src/processors/TokenCompressor.d.ts +2 -0
  66. package/ts_build/src/processors/TokenCompressor.js +57 -7
  67. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  68. package/ts_build/src/services/AgentSyncFs.d.ts +1 -0
  69. package/ts_build/src/services/AgentSyncFs.js +21 -4
  70. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  71. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -0
  72. package/ts_build/src/services/AgentSyncKnowhowWeb.js +21 -4
  73. package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
  74. package/ts_build/src/services/SyncedAgentWatcher.d.ts +3 -0
  75. package/ts_build/src/services/SyncedAgentWatcher.js +4 -0
  76. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  77. package/ts_build/src/services/conversion/ConversionService.d.ts +18 -0
  78. package/ts_build/src/services/conversion/ConversionService.js +585 -0
  79. package/ts_build/src/services/conversion/ConversionService.js.map +1 -0
  80. package/ts_build/src/services/conversion/index.d.ts +2 -0
  81. package/ts_build/src/services/conversion/index.js +19 -0
  82. package/ts_build/src/services/conversion/index.js.map +1 -0
  83. package/ts_build/src/services/conversion/types.d.ts +49 -0
  84. package/ts_build/src/services/conversion/types.js +3 -0
  85. package/ts_build/src/services/conversion/types.js.map +1 -0
  86. package/ts_build/src/services/index.d.ts +3 -0
  87. package/ts_build/src/services/index.js +6 -1
  88. package/ts_build/src/services/index.js.map +1 -1
  89. package/ts_build/src/services/modules/index.d.ts +2 -0
  90. package/ts_build/src/services/modules/types.d.ts +2 -0
  91. package/ts_build/src/services/watchers/FsSyncer.d.ts +1 -0
  92. package/ts_build/src/services/watchers/FsSyncer.js +5 -0
  93. package/ts_build/src/services/watchers/FsSyncer.js.map +1 -1
  94. package/ts_build/src/services/watchers/RemoteSyncer.d.ts +1 -0
  95. package/ts_build/src/services/watchers/RemoteSyncer.js +4 -0
  96. package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -1
  97. package/ts_build/tests/agents/tools/readFile.test.d.ts +1 -0
  98. package/ts_build/tests/agents/tools/readFile.test.js +90 -0
  99. package/ts_build/tests/agents/tools/readFile.test.js.map +1 -0
  100. package/ts_build/tests/clients/AIClient.test.js +1 -0
  101. package/ts_build/tests/clients/AIClient.test.js.map +1 -1
  102. package/ts_build/tests/clients/contextLimits.test.d.ts +1 -0
  103. package/ts_build/tests/clients/contextLimits.test.js +57 -0
  104. package/ts_build/tests/clients/contextLimits.test.js.map +1 -0
  105. package/ts_build/tests/patching/patchFileOutput.test.d.ts +1 -0
  106. package/ts_build/tests/patching/patchFileOutput.test.js +187 -0
  107. package/ts_build/tests/patching/patchFileOutput.test.js.map +1 -0
  108. package/ts_build/tests/patching/regression-2026.test.js +214 -0
  109. package/ts_build/tests/patching/regression-2026.test.js.map +1 -1
  110. package/ts_build/tests/processors/CustomVariables.test.js +4 -4
  111. package/ts_build/tests/processors/CustomVariables.test.js.map +1 -1
  112. package/ts_build/tests/processors/TokenCompressor.test.js +37 -1
  113. package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
  114. package/ts_build/tests/processors/tools/grepToolResponse.test.d.ts +1 -0
  115. package/ts_build/tests/processors/tools/grepToolResponse.test.js +40 -0
  116. package/ts_build/tests/processors/tools/grepToolResponse.test.js.map +1 -0
  117. package/ts_build/tests/services/ConversionService.test.d.ts +1 -0
  118. package/ts_build/tests/services/ConversionService.test.js +154 -0
  119. package/ts_build/tests/services/ConversionService.test.js.map +1 -0
  120. package/ts_build/tests/test.spec.js +1 -1
  121. package/ts_build/tests/test.spec.js.map +1 -1
  122. package/ts_build/tests/unit/clients/AIClient.test.js +3 -0
  123. package/ts_build/tests/unit/clients/AIClient.test.js.map +1 -1
@@ -32,6 +32,12 @@ export class TokenCompressor implements JsonCompressorStorage {
32
32
  private compressionThreshold: number = 4000;
33
33
  private characterLimit: number = this.compressionThreshold * 4;
34
34
 
35
+ // Minimum size (in characters) a standalone chunk should have. Leftover content
36
+ // smaller than this is merged into an adjacent chunk so we never emit tiny,
37
+ // low-value chunks that force the agent into extra round-trips to reassemble a
38
+ // file. Defaults to half of the per-chunk character limit.
39
+ private minChunkCharacters: number = Math.floor(this.characterLimit / 2);
40
+
35
41
  // Largest size retrievable without re-compressing
36
42
  public maxTokens: number = this.compressionThreshold * 2;
37
43
 
@@ -56,6 +62,7 @@ export class TokenCompressor implements JsonCompressorStorage {
56
62
  public setCompressionThreshold(threshold: number): void {
57
63
  this.compressionThreshold = threshold;
58
64
  this.characterLimit = threshold * 4; // Update character limit based on new threshold
65
+ this.minChunkCharacters = Math.floor(this.characterLimit / 2);
59
66
  this.jsonCompressor.updateSettings(threshold, this.maxTokens);
60
67
  }
61
68
 
@@ -88,7 +95,7 @@ export class TokenCompressor implements JsonCompressorStorage {
88
95
  const chunkKeys: string[] = [];
89
96
  let remaining = content;
90
97
 
91
- // Split from the end, creating chunks that will be linked
98
+ // Split from the end, creating chunks that will be linked.
92
99
  while (remaining.length > this.characterLimit) {
93
100
  const chunkStart = remaining.length - this.characterLimit;
94
101
  const chunk = remaining.substring(chunkStart);
@@ -96,9 +103,19 @@ export class TokenCompressor implements JsonCompressorStorage {
96
103
  remaining = remaining.substring(0, chunkStart);
97
104
  }
98
105
 
99
- // The remaining part becomes the first chunk
106
+ // The remaining part becomes the first chunk. To avoid producing a tiny,
107
+ // low-value leading chunk (which forces the agent into extra round-trips to
108
+ // reassemble a file), merge it into the next chunk when it falls below the
109
+ // minimum chunk size. Only split it out as its own chunk when it is large
110
+ // enough to stand on its own.
100
111
  if (remaining.length > 0) {
101
- chunks.unshift(remaining);
112
+ if (chunks.length > 0 && remaining.length < this.minChunkCharacters) {
113
+ // Prepend the small leftover onto the first existing chunk so every
114
+ // emitted chunk carries a meaningful amount of content.
115
+ chunks[0] = remaining + chunks[0];
116
+ } else {
117
+ chunks.unshift(remaining);
118
+ }
102
119
  }
103
120
 
104
121
  // Store chunks and create chain of references
@@ -131,7 +148,7 @@ export class TokenCompressor implements JsonCompressorStorage {
131
148
  200
132
149
  )}...\n[Use ${
133
150
  this.toolName
134
- } tool with key "${firstKey}" to retrieve content. Follow NEXT_CHUNK_KEY references for complete content]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map without repeated expandTokens calls]`;
151
+ } tool with key "${firstKey}" to retrieve the FULL content in one call (chunks are auto-stitched; pass fromLine/toLine for a ranged read)]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map with REAL line numbers without expanding at all]`;
135
152
  }
136
153
 
137
154
  /**
@@ -282,6 +299,38 @@ export class TokenCompressor implements JsonCompressorStorage {
282
299
  return this.storage[key] || null;
283
300
  }
284
301
 
302
+ /**
303
+ * Retrieves the fully reassembled content for a key, following any
304
+ * `NEXT_CHUNK_KEY` references and stripping the chunk-linking markers. This
305
+ * lets callers get the complete content in a single call instead of chasing a
306
+ * chain of nested keys.
307
+ */
308
+ retrieveFullString(key: string): string | null {
309
+ let currentKey: string | null = key;
310
+ let result = "";
311
+ let found = false;
312
+ const visited = new Set<string>();
313
+
314
+ while (currentKey && !visited.has(currentKey)) {
315
+ visited.add(currentKey);
316
+ const chunk = this.storage[currentKey];
317
+ if (chunk === undefined) {
318
+ break;
319
+ }
320
+ found = true;
321
+
322
+ const nextMatch = chunk.match(/\n\n\[NEXT_CHUNK_KEY:\s*([^\s\]]+)\]/);
323
+ const nextKey = nextMatch ? nextMatch[1] : null;
324
+
325
+ // Strip the trailing NEXT_CHUNK_KEY marker before stitching.
326
+ const cleaned = chunk.replace(/\n\n\[NEXT_CHUNK_KEY:\s*[^\s\]]+\]/, "");
327
+ result += cleaned;
328
+ currentKey = nextKey;
329
+ }
330
+
331
+ return found ? result : null;
332
+ }
333
+
285
334
  storeString(key: string, value: string): void {
286
335
  if (this.estimateTokens(value) > this.maxTokens) {
287
336
  // adjust max tokens so we can always retrieve this without re-compressing
@@ -307,15 +356,42 @@ export class TokenCompressor implements JsonCompressorStorage {
307
356
  if (toolsService) {
308
357
  toolsService.addTools([expandTokensDefinition]);
309
358
  toolsService.addFunctions({
310
- [this.toolName]: (key: string) => {
311
- const data = this.retrieveString(key);
359
+ [this.toolName]: (key: string, fromLine?: number, toLine?: number) => {
360
+ // Auto-stitch: follow any NEXT_CHUNK_KEY chain and return the full,
361
+ // reassembled content so the agent never has to chase nested keys.
362
+ const data = this.retrieveFullString(key);
312
363
 
313
- if (!data) {
364
+ if (data === null) {
314
365
  return `Error: No data found for key "${key}". Available keys: ${this.getStorageKeys().join(
315
366
  ", "
316
367
  )}`;
317
368
  }
318
- return data;
369
+
370
+ // Optional ranged read: return only the requested 1-based, inclusive
371
+ // line range, prefixed with real line numbers for easy mapping back.
372
+ const hasRange =
373
+ typeof fromLine === "number" || typeof toLine === "number";
374
+ if (!hasRange) {
375
+ return data;
376
+ }
377
+
378
+ const lines = data.split("\n");
379
+ const totalLines = lines.length;
380
+ const start = Math.max(1, typeof fromLine === "number" ? fromLine : 1);
381
+ const end = Math.min(
382
+ totalLines,
383
+ typeof toLine === "number" ? toLine : totalLines
384
+ );
385
+
386
+ if (start > end) {
387
+ return `Error: Invalid line range for key "${key}": fromLine (${start}) is greater than toLine (${end}). Content has ${totalLines} lines.`;
388
+ }
389
+
390
+ const numbered: string[] = [];
391
+ for (let i = start; i <= end; i++) {
392
+ numbered.push(`${i}: ${lines[i - 1]}`);
393
+ }
394
+ return numbered.join("\n");
319
395
  },
320
396
  });
321
397
  }
@@ -420,7 +496,7 @@ export const expandTokensDefinition: Tool = {
420
496
  function: {
421
497
  name: "expandTokens",
422
498
  description:
423
- "Retrieve a chunk of compressed data that was stored during message processing. The returned content may contain a `NEXT_CHUNK_KEY` to retrieve subsequent chunks. NOTE: Can also use jqToolResponse, grepToolResponse, tailToolResponse to access/filter/search compressed content without needing to expand all tokens.",
499
+ "Retrieve compressed data that was stored during message processing. The full content is automatically reassembled (any chunk chain is followed for you, so you never need to chase NEXT_CHUNK_KEY references). Optionally pass fromLine/toLine (1-based, inclusive) to return just a range of lines, prefixed with real line numbers. NOTE: Can also use jqToolResponse, grepToolResponse, tailToolResponse to access/filter/search compressed content without needing to expand all tokens.",
424
500
  parameters: {
425
501
  type: "object",
426
502
  positional: true,
@@ -429,6 +505,16 @@ export const expandTokensDefinition: Tool = {
429
505
  type: "string",
430
506
  description: "The key of the compressed data to retrieve",
431
507
  },
508
+ fromLine: {
509
+ type: "number",
510
+ description:
511
+ "Optional 1-based start line (inclusive) for a ranged read of the reassembled content.",
512
+ },
513
+ toLine: {
514
+ type: "number",
515
+ description:
516
+ "Optional 1-based end line (inclusive). Defaults to the end of the content when omitted.",
517
+ },
432
518
  },
433
519
  required: ["key"],
434
520
  },
@@ -220,10 +220,15 @@ export class AgentSyncFs {
220
220
  );
221
221
  this.lastInputContent = input;
222
222
 
223
- agent.addPendingUserMessage({
224
- role: "user",
225
- content: input,
226
- });
223
+ // Detect slash commands in the input
224
+ if (input.trim().startsWith("/")) {
225
+ await this.handleInputCommand(agent, input.trim());
226
+ } else {
227
+ agent.addPendingUserMessage({
228
+ role: "user",
229
+ content: input,
230
+ });
231
+ }
227
232
 
228
233
  // Clear the input file after processing
229
234
  await this.writeInput("");
@@ -233,6 +238,23 @@ export class AgentSyncFs {
233
238
  }
234
239
  }
235
240
 
241
+ /**
242
+ * Handle slash commands sent via input.txt
243
+ */
244
+ private async handleInputCommand(agent: BaseAgent, input: string): Promise<void> {
245
+ const [command, ...rest] = input.split(" ");
246
+ const message = rest.join(" ").trim() || undefined;
247
+
248
+ if (command === "/poke") {
249
+ console.log(`🫵 Received /poke command for task ${this.taskId}`);
250
+ agent.interrupt(message);
251
+ } else {
252
+ // Unknown command — treat as a regular message
253
+ console.warn(`⚠️ Unknown command "${command}", treating as message`);
254
+ agent.addPendingUserMessage({ role: "user", content: input });
255
+ }
256
+ }
257
+
236
258
  /**
237
259
  * Wait for resume by monitoring status file
238
260
  */
@@ -151,10 +151,15 @@ export class AgentSyncKnowhowWeb {
151
151
  // Inject pending messages into the agent
152
152
  const messageIds: string[] = [];
153
153
  for (const msg of pendingMessages) {
154
- agent.addPendingUserMessage({
155
- role: msg.role as "user" | "assistant",
156
- content: msg.message,
157
- });
154
+ // Detect slash commands in messages
155
+ if (msg.message.trim().startsWith("/")) {
156
+ await this.handleMessageCommand(agent, msg.message.trim());
157
+ } else {
158
+ agent.addPendingUserMessage({
159
+ role: msg.role as "user" | "assistant",
160
+ content: msg.message,
161
+ });
162
+ }
158
163
  messageIds.push(msg.id);
159
164
  }
160
165
 
@@ -167,6 +172,23 @@ export class AgentSyncKnowhowWeb {
167
172
  }
168
173
  }
169
174
 
175
+ /**
176
+ * Handle slash commands sent as pending messages
177
+ */
178
+ private async handleMessageCommand(agent: BaseAgent, input: string): Promise<void> {
179
+ const [command, ...rest] = input.split(" ");
180
+ const message = rest.join(" ").trim() || undefined;
181
+
182
+ if (command === "/poke") {
183
+ console.log(`🫵 Received /poke command for task ${this.knowhowTaskId}`);
184
+ agent.interrupt(message);
185
+ } else {
186
+ // Unknown command — treat as a regular message
187
+ console.warn(`⚠️ Unknown command "${command}", treating as message`);
188
+ agent.addPendingUserMessage({ role: "user", content: input });
189
+ }
190
+ }
191
+
170
192
  /**
171
193
  * Wait for the agent to be resumed or killed via API
172
194
  * Polls the API every 2 seconds, with a 1 hour timeout
@@ -13,6 +13,8 @@ export interface SyncedAgentWatcher {
13
13
  stopWatching(): void;
14
14
  /** The task ID being watched */
15
15
  taskId: string;
16
+ /** Interrupt the agent's current tool call or AI completion */
17
+ interrupt(message?: string): Promise<void>;
16
18
  /** The agent name being watched */
17
19
  agentName: string;
18
20
  /** Send a message to the remote agent */
@@ -55,6 +57,7 @@ export interface AttachableAgent {
55
57
  pause(): void | Promise<void>;
56
58
  unpause(): void | Promise<void>;
57
59
  kill(): void | Promise<void>;
60
+ interrupt(message?: string): void | Promise<void>;
58
61
  addPendingUserMessage(message: Message): void;
59
62
  }
60
63
 
@@ -97,6 +100,11 @@ export class WatcherBackedAgent implements AttachableAgent {
97
100
  this.agentEvents.emit(this.eventTypes.done, "Agent killed");
98
101
  }
99
102
 
103
+ async interrupt(message?: string): Promise<void> {
104
+ await this.watcher.interrupt(message);
105
+ console.log("🫵 Interrupt sent to remote agent.");
106
+ }
107
+
100
108
  addPendingUserMessage(message: Message): void {
101
109
  // Fire-and-forget — errors are logged but not surfaced
102
110
  const text = typeof message.content === "string" ? message.content : "";