@dugleelabs/copair 1.6.0 → 1.8.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/api.d.ts CHANGED
@@ -81,6 +81,10 @@ declare const ProviderConfigSchema: z.ZodObject<{
81
81
  }, z.core.$strip>>;
82
82
  timeout_ms: z.ZodOptional<z.ZodNumber>;
83
83
  }, z.core.$strip>;
84
+ declare const SmallModelsConfigSchema: z.ZodObject<{
85
+ model_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
86
+ max_tool_calls: z.ZodOptional<z.ZodNumber>;
87
+ }, z.core.$strip>;
84
88
  declare const CopairConfigSchema: z.ZodObject<{
85
89
  version: z.ZodNumber;
86
90
  default_model: z.ZodOptional<z.ZodString>;
@@ -172,9 +176,14 @@ declare const CopairConfigSchema: z.ZodObject<{
172
176
  web_search_timeout_ms: z.ZodDefault<z.ZodNumber>;
173
177
  provider_timeout_ms: z.ZodDefault<z.ZodNumber>;
174
178
  }, z.core.$strip>>;
179
+ small_models: z.ZodOptional<z.ZodObject<{
180
+ model_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
181
+ max_tool_calls: z.ZodOptional<z.ZodNumber>;
182
+ }, z.core.$strip>>;
175
183
  }, z.core.$strip>;
176
184
  type CopairConfig = z.infer<typeof CopairConfigSchema>;
177
185
  type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
186
+ type SmallModelsConfig = z.infer<typeof SmallModelsConfigSchema>;
178
187
 
179
188
  type ProviderFactory = (config: ProviderConfig, model: string) => Provider;
180
189
  declare class ProviderRegistry {
@@ -216,21 +225,42 @@ declare class ToolRegistry {
216
225
  getAllDefinitions(): ToolDefinition[];
217
226
  }
218
227
 
228
+ /**
229
+ * Path-level permissions — model-agnostic.
230
+ * Applies regardless of which tool the model uses (read, bash cat, grep, etc.).
231
+ * write permission implies read.
232
+ */
233
+ interface PathPermissions {
234
+ read: string[];
235
+ write: string[];
236
+ }
219
237
  interface AllowRules {
220
238
  /** bash entries: exact match, or prefix if the pattern ends with " *" */
221
239
  bash: string[];
222
240
  /**
223
241
  * git entries: matched against the args string.
224
- * Entry is the subcommand (e.g. "diff") — automatically covers all
225
- * flags for that subcommand (e.g. "diff --cached", "diff HEAD~1").
242
+ * Entry is the subcommand (e.g. "diff") — covers all flags for that subcommand.
226
243
  */
227
244
  git: string[];
228
245
  /**
229
- * write / edit entries: glob patterns matched against the file path.
230
- * Supports * (within a segment) and ** (across segments).
246
+ * Tool-specific read/write/edit entries (kept for backward-compat and
247
+ * fine-grained tool control). For multi-model setups, prefer `paths:`.
231
248
  */
249
+ read: string[];
232
250
  write: string[];
233
251
  edit: string[];
252
+ /**
253
+ * Path-level permissions — the preferred way to allow cross-repo access.
254
+ * Works regardless of which tool the model chooses to use for an operation.
255
+ *
256
+ * Example allow.yaml:
257
+ * paths:
258
+ * read:
259
+ * - "../../other-repo/**"
260
+ * write:
261
+ * - "../../shared-output/**"
262
+ */
263
+ paths: PathPermissions;
234
264
  }
235
265
  declare class AllowList {
236
266
  private rules;
@@ -238,11 +268,25 @@ declare class AllowList {
238
268
  /**
239
269
  * Returns true when the operation is explicitly listed and should bypass
240
270
  * the approval prompt. Called by ApprovalGate before prompting.
271
+ *
272
+ * Check order per tool:
273
+ * 1. Tool-specific entries (bash:, git:, read:, write:, edit:) — fine-grained
274
+ * 2. Path-level entries (paths.read / paths.write) — model-agnostic
241
275
  */
242
276
  matches(toolName: string, input: Record<string, unknown>): boolean;
243
277
  private matchBash;
278
+ /**
279
+ * Path-level bash matching: extract path tokens from the command, classify
280
+ * as read or write intent, then check against paths.read / paths.write.
281
+ * All path tokens in the command must be covered for the check to pass.
282
+ */
283
+ private matchBashByPath;
244
284
  private matchGit;
245
285
  private matchPath;
286
+ /**
287
+ * Check filePath against paths.read or paths.write (write implies read).
288
+ */
289
+ private matchPathAgainstPermissions;
246
290
  }
247
291
 
248
292
  interface ToolInfo {
@@ -275,15 +319,27 @@ interface TokenUsage {
275
319
  contextPercent?: number;
276
320
  }
277
321
  type ApprovalAnswer = 'allow' | 'always' | 'deny' | 'all' | 'similar';
322
+ /** Pre-approval diff shown at the approval prompt (before execution). */
323
+ interface ApprovalDiffPreview {
324
+ filePath: string;
325
+ oldContent: string | null;
326
+ newContent: string;
327
+ diffText: string;
328
+ }
278
329
  interface ApprovalRequest {
279
330
  toolName: string;
280
331
  input: Record<string, unknown>;
281
332
  summary: string;
282
333
  index: number;
283
334
  total: number;
284
- diff?: DiffInfo;
335
+ /** Pre-approval diff preview (shown before user approves). */
336
+ diff?: ApprovalDiffPreview | null;
285
337
  /** Present when a bash command references a sensitive system path. */
286
338
  warning?: string;
339
+ /** Present when a bash command references a path outside the project root. */
340
+ crossRepoBashPath?: string;
341
+ /** Present when a read/glob/grep targets a path outside the project root. */
342
+ crossRepoReadPath?: string;
287
343
  }
288
344
  interface AgentBridgeEvents {
289
345
  'stream-text': (text: string) => void;
@@ -297,11 +353,22 @@ interface AgentBridgeEvents {
297
353
  'approval-request': (request: ApprovalRequest, respond: (answer: ApprovalAnswer) => void) => void;
298
354
  'diff': (diff: DiffInfo) => void;
299
355
  'usage': (usage: TokenUsage) => void;
300
- 'thinking-start': () => void;
356
+ 'thinking-start': (label?: string) => void;
301
357
  'thinking-stop': () => void;
302
358
  'turn-complete': () => void;
303
359
  'error': (message: string) => void;
304
- 'input-request': (respond: (input: string) => void) => void;
360
+ 'input-request': (prompt: string, respond: (input: string) => void) => void;
361
+ 'context-limit-warning': () => void;
362
+ 'context-limit-action': (respond: (action: 'compact' | 'abort') => void) => void;
363
+ 'task-complete': (data: {
364
+ summary: string;
365
+ }) => void;
366
+ 'max-turn-warning': (data: {
367
+ limit: number;
368
+ }) => void;
369
+ 'unclear-signal': (data: {
370
+ message: string;
371
+ }) => void;
305
372
  }
306
373
  type EventName = keyof AgentBridgeEvents;
307
374
  /**
@@ -335,8 +402,8 @@ declare class AgentBridge extends EventEmitter {
335
402
  * input_summary is always redacted and truncated to ≤ 200 chars before
336
403
  * writing so that raw secrets never appear in the audit log.
337
404
  */
338
- type AuditEvent = 'session_start' | 'session_end' | 'tool_call' | 'approval' | 'denial' | 'path_block' | 'schema_rejection' | 'bash_sensitive_path';
339
- type AuditOutcome = 'allowed' | 'denied' | 'error';
405
+ type AuditEvent = 'session_start' | 'session_end' | 'tool_call' | 'approval' | 'denial' | 'path_block' | 'schema_rejection' | 'bash_sensitive_path' | 'bash_cross_repo' | 'cross_repo_read';
406
+ type AuditOutcome = 'allowed' | 'denied' | 'error' | 'flagged';
340
407
  interface AuditEntry {
341
408
  ts: string;
342
409
  event: AuditEvent;
@@ -386,7 +453,7 @@ declare class ApprovalGate {
386
453
  * The agent never calls this. ToolExecutor calls it. The agent only
387
454
  * sees the resulting ExecutionResult.
388
455
  */
389
- allow(toolName: string, input: Record<string, unknown>): Promise<boolean>;
456
+ allow(toolName: string, input: Record<string, unknown>, diffPreview?: ApprovalDiffPreview): Promise<boolean>;
390
457
  /** Bridge-based approval: emit event and await response from ink UI. */
391
458
  private bridgePrompt;
392
459
  /** Legacy approval prompt: reads from /dev/tty directly (not stdin).
@@ -455,7 +522,15 @@ declare class PathGuard {
455
522
  * @param mustExist true for read operations (file must exist); false for
456
523
  * write/edit operations (parent dir must exist).
457
524
  */
458
- check(rawPath: string, mustExist: boolean): PathGuardResult;
525
+ check(rawPath: string, mustExist: boolean, opts?: {
526
+ skipBoundaryCheck?: boolean;
527
+ }): PathGuardResult;
528
+ /**
529
+ * Check whether a raw path resolves to somewhere inside the project root.
530
+ * Used by the tool executor to flag cross-repo references before the gate fires.
531
+ * Returns false on any resolution error — treat as outside.
532
+ */
533
+ isInsideProject(rawPath: string): boolean;
459
534
  private isDenied;
460
535
  private isAllowed;
461
536
  /**
@@ -655,8 +730,43 @@ declare class ConversationManager {
655
730
  static fromJSONL(data: string): Message[];
656
731
  }
657
732
 
733
+ interface ParsedToolCall {
734
+ id: string;
735
+ name: string;
736
+ arguments: string;
737
+ }
738
+ interface ToolCallFormatter {
739
+ readonly name: string;
740
+ readonly markupPattern: RegExp;
741
+ /** Opening tag for streaming suppression (e.g. `<tool_call>`). When set,
742
+ * the renderer buffers across chunks instead of applying a per-chunk regex. */
743
+ readonly openTag?: string;
744
+ readonly closeTag?: string;
745
+ /** When true, the streaming filter also suppresses all text that follows the
746
+ * first complete tool-call block in a response. Use for models that
747
+ * hallucinate post-tool text (e.g. "It seems there was an issue…"). */
748
+ readonly suppressAfterMatch?: boolean;
749
+ parse(text: string): {
750
+ toolCalls: ParsedToolCall[];
751
+ remainingText: string;
752
+ };
753
+ buildSystemPrompt(tools: ToolDefinition$1[]): string;
754
+ /** Return a minimal example tool call in this formatter's markup language. */
755
+ exampleCall(): string;
756
+ }
757
+
658
758
  type FormatName = 'dsml' | 'qwen-xml' | 'fenced-block';
659
759
 
760
+ declare class SmallModelHarness {
761
+ readonly isSmallModel: boolean;
762
+ private config;
763
+ constructor(modelId: string, config?: SmallModelsConfig, forceOverride?: boolean);
764
+ get maxToolCalls(): number;
765
+ getSystemPromptAddition(): string | null;
766
+ getPerTurnReminder(): string | null;
767
+ getFormatHint(formatter: ToolCallFormatter): string | null;
768
+ }
769
+
660
770
  interface AgentOptions {
661
771
  systemPrompt?: string;
662
772
  maxTokens?: number;
@@ -664,6 +774,9 @@ interface AgentOptions {
664
774
  toolCallFormat?: FormatName;
665
775
  bridge?: AgentBridge;
666
776
  pluginManager?: PluginManager;
777
+ /** Fraction of maxTokens at which to warn about context limit (0–1, default 0.9). */
778
+ contextLimitThresholdPct?: number;
779
+ harness?: SmallModelHarness;
667
780
  }
668
781
  declare class Agent {
669
782
  private provider;
@@ -677,6 +790,7 @@ declare class Agent {
677
790
  private formatter;
678
791
  private textFilter;
679
792
  private pluginManager?;
793
+ private harness?;
680
794
  constructor(provider: Provider, model: string, toolRegistry: ToolRegistry, executor: ToolExecutor, options?: AgentOptions);
681
795
  get model(): string;
682
796
  getConversation(): ConversationManager;
@@ -693,6 +807,16 @@ declare class Agent {
693
807
  /** Input tokens from the last API call — reflects actual context window usage. */
694
808
  lastInputTokens: number;
695
809
  }>;
810
+ /** Prompt the user for input and return their answer (used by ask_user intercept). */
811
+ private collectUserAnswer;
812
+ /**
813
+ * Detect whether the model likely hit its context limit this turn.
814
+ * Two signals:
815
+ * 1. Token threshold: input tokens ≥ contextLimitThresholdPct of maxTokens
816
+ * 2. Truncation heuristic: text present, no tool calls, and response ends
817
+ * without terminal punctuation (sentence was cut off mid-stream)
818
+ */
819
+ private detectContextLimit;
696
820
  }
697
821
 
698
822
  interface SessionMetadata {