@sayknow-cli/coding-agent 0.3.2 → 0.3.5

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 (65) hide show
  1. package/dist/types/config/model-profiles.d.ts +2 -2
  2. package/dist/types/config/model-registry.d.ts +3 -3
  3. package/dist/types/config/models-config-schema.d.ts +0 -5
  4. package/dist/types/config/settings-schema.d.ts +78 -9
  5. package/dist/types/export/html/template.generated.d.ts +1 -1
  6. package/dist/types/skc-runtime/state-renderer.d.ts +5 -0
  7. package/dist/types/skc-runtime/ultragoal-guard.d.ts +37 -1
  8. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +80 -0
  9. package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
  10. package/dist/types/tools/computer-gc.d.ts +23 -0
  11. package/dist/types/tools/cron.d.ts +36 -61
  12. package/dist/types/tools/index.d.ts +0 -1
  13. package/dist/types/tools/resource-gc.d.ts +54 -0
  14. package/dist/types/web/search/index.d.ts +1 -0
  15. package/dist/types/web/search/providers/utils.d.ts +11 -4
  16. package/package.json +7 -7
  17. package/scripts/verify-insane-vendor.ts +5 -0
  18. package/src/cli/args.ts +0 -1
  19. package/src/cli/fast-help.ts +0 -1
  20. package/src/cli/plugin-cli.ts +1 -1
  21. package/src/cli/web-search-cli.ts +5 -0
  22. package/src/config/model-profile-activation.ts +7 -1
  23. package/src/config/model-profiles.ts +3 -4
  24. package/src/config/model-registry.ts +3 -6
  25. package/src/config/models-config-schema.ts +1 -1
  26. package/src/config/settings-schema.ts +80 -10
  27. package/src/export/html/template.generated.ts +1 -1
  28. package/src/export/html/template.js +0 -12
  29. package/src/goals/tools/goal-tool.ts +14 -1
  30. package/src/internal-urls/docs-index.generated.ts +3 -4
  31. package/src/modes/components/model-selector.ts +9 -1
  32. package/src/modes/controllers/selector-controller.ts +6 -0
  33. package/src/prompts/system/system-prompt.md +16 -20
  34. package/src/prompts/tools/cron.md +5 -3
  35. package/src/prompts/tools/read.md +1 -1
  36. package/src/sdk.ts +5 -0
  37. package/src/session/agent-session.ts +44 -0
  38. package/src/skc-runtime/state-renderer.ts +13 -0
  39. package/src/skc-runtime/ultragoal-guard.ts +167 -0
  40. package/src/skc-runtime/ultragoal-runtime.ts +221 -1
  41. package/src/tools/browser/tab-supervisor.ts +86 -2
  42. package/src/tools/computer-gc.ts +66 -0
  43. package/src/tools/computer.ts +2 -0
  44. package/src/tools/cron.ts +75 -112
  45. package/src/tools/index.ts +2 -8
  46. package/src/tools/read.ts +25 -55
  47. package/src/tools/renderers.ts +0 -2
  48. package/src/tools/resource-gc.ts +291 -0
  49. package/src/tools/ultragoal-ask-guard.ts +7 -1
  50. package/src/web/search/index.ts +1 -0
  51. package/src/web/search/providers/utils.ts +22 -5
  52. package/vendor/insane-search/MANIFEST.json +5 -1
  53. package/vendor/insane-search/engine/__init__.py +14 -0
  54. package/vendor/insane-search/engine/content_safety.py +151 -0
  55. package/vendor/insane-search/engine/fetch_chain.py +32 -0
  56. package/vendor/insane-search/engine/tests/test_u1.py +28 -0
  57. package/vendor/insane-search/engine/tests/test_u8.py +216 -0
  58. package/vendor/insane-search/engine/transport.py +3 -2
  59. package/vendor/insane-search/engine/validators.py +14 -0
  60. package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
  61. package/dist/types/tools/inspect-image.d.ts +0 -31
  62. package/src/prompts/tools/inspect-image-system.md +0 -20
  63. package/src/prompts/tools/inspect-image.md +0 -32
  64. package/src/tools/inspect-image-renderer.ts +0 -103
  65. package/src/tools/inspect-image.ts +0 -172
@@ -39,6 +39,11 @@ export declare function renderUltragoalStatusMarkdown(summary: {
39
39
  };
40
40
  counts: Record<string, number>;
41
41
  goals: unknown[];
42
+ nudgeBudget?: number;
43
+ nudgeCount?: number;
44
+ nudgeRemaining?: number;
45
+ nudgeGoalId?: string;
46
+ nudgeTargetKind?: string;
42
47
  }): string;
43
48
  export declare function renderTeamStatusMarkdown(snapshot: {
44
49
  team_name: string;
@@ -1,4 +1,4 @@
1
- import { type UltragoalGoal, type UltragoalLedgerEvent, type UltragoalPlan, type UltragoalReceiptKind } from "./ultragoal-runtime";
1
+ import { type UltragoalGoal, type UltragoalLedgerEvent, type UltragoalNudgeSurface, type UltragoalPlan, type UltragoalReceiptKind } from "./ultragoal-runtime";
2
2
  export type UltragoalGuardState = "inactive" | "unrelated_goal" | "active_verified_complete" | "active_missing_receipt" | "active_stale_receipt" | "active_missing_final_receipt" | "active_dirty_quality_gate" | "active_review_blocked_unrecorded" | "active_review_blocked_recorded" | "unreadable_fail_closed";
3
3
  export interface UltragoalGuardDiagnostic {
4
4
  state: UltragoalGuardState;
@@ -30,6 +30,29 @@ export declare function readUltragoalVerificationState(input: {
30
30
  sessionId?: string | null;
31
31
  }): Promise<UltragoalGuardDiagnostic>;
32
32
  export declare function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBlockDiagnostic>;
33
+ /**
34
+ * Escalating per-attempt refusal text. Deliberately avoids every
35
+ * `isUltragoalBypassPrompt` trigger: no `update_goal(`, no "skip/weaken verification",
36
+ * no "mark ... complete", no "--status complete", and the word "complete" never
37
+ * appears (so the `goal` ... `complete` proximity rule cannot match).
38
+ */
39
+ export declare function formatUltragoalNudgeMessage(input: {
40
+ surface: UltragoalNudgeSurface;
41
+ attempt: number;
42
+ budget: number;
43
+ goalId: string;
44
+ }): string;
45
+ /**
46
+ * Assert-path entry for the `ask` surface (the ask guard lives in another module).
47
+ * Resolves the active (leader) Ultragoal session so subagent/headless asks consume
48
+ * the leader run's budget rather than a fresh child ledger.
49
+ */
50
+ export declare function consumeUltragoalAskNudge(cwd: string, sessionId?: string | null): Promise<{
51
+ nudged: true;
52
+ message: string;
53
+ } | {
54
+ nudged: false;
55
+ }>;
33
56
  export declare function assertCanCompleteCurrentGoal(input: {
34
57
  cwd: string;
35
58
  currentGoal?: CurrentGoalLike | null;
@@ -49,3 +72,16 @@ export interface UltragoalPauseBlockDiagnostic {
49
72
  */
50
73
  export declare function isUltragoalPauseBlocked(cwd: string): Promise<UltragoalPauseBlockDiagnostic>;
51
74
  export declare function assertUltragoalPauseAllowed(cwd: string): Promise<void>;
75
+ /**
76
+ * Guard `goal({"op":"drop"})` during an active Ultragoal run. A *real give-up* (an
77
+ * aggregate run still mid-flight with incomplete required stories) is nudged while
78
+ * budget remains; once exhausted it falls through to today's drop behavior. Legitimate
79
+ * aggregate-reset drops — no durable run, unrelated goal, an already dropped/stale
80
+ * aggregate, or an all-stories-complete run — are never nudged. If durable state
81
+ * exists but cannot be read to classify the drop, fail closed.
82
+ */
83
+ export declare function assertUltragoalDropAllowed(input: {
84
+ cwd: string;
85
+ currentGoal?: CurrentGoalLike | null;
86
+ sessionId?: string | null;
87
+ }): Promise<void>;
@@ -53,6 +53,41 @@ export interface UltragoalLedgerEvent extends JsonObject {
53
53
  goalId?: string;
54
54
  timestamp?: string;
55
55
  }
56
+ export type UltragoalNudgeSurface = "pause" | "drop" | "ask" | "premature_complete";
57
+ export type UltragoalNudgeTargetKind = "story" | "final_aggregate_receipt";
58
+ export interface UltragoalNudgeLedgerEvent extends UltragoalLedgerEvent {
59
+ event: "nudge";
60
+ goalId: string;
61
+ targetKind: UltragoalNudgeTargetKind;
62
+ surface: UltragoalNudgeSurface;
63
+ attempt: number;
64
+ budget: number;
65
+ reason: string;
66
+ currentGoalObjective?: string;
67
+ }
68
+ export interface UltragoalNudgeTarget {
69
+ goalId: string;
70
+ targetKind: UltragoalNudgeTargetKind;
71
+ }
72
+ export type UltragoalNudgeOutcome = {
73
+ nudged: true;
74
+ attempt: number;
75
+ budget: number;
76
+ goalId: string;
77
+ targetKind: UltragoalNudgeTargetKind;
78
+ event: UltragoalNudgeLedgerEvent;
79
+ } | {
80
+ nudged: false;
81
+ exhausted: true;
82
+ count: number;
83
+ budget: number;
84
+ goalId: string;
85
+ targetKind: UltragoalNudgeTargetKind;
86
+ } | {
87
+ nudged: false;
88
+ inactive: true;
89
+ reason: string;
90
+ };
56
91
  export interface UltragoalPaths {
57
92
  dir: string;
58
93
  briefPath: string;
@@ -67,6 +102,11 @@ export interface UltragoalStatusSummary {
67
102
  currentGoal?: UltragoalGoal;
68
103
  counts: Record<UltragoalGoalStatus, number>;
69
104
  goals: UltragoalGoal[];
105
+ nudgeBudget?: number;
106
+ nudgeCount?: number;
107
+ nudgeRemaining?: number;
108
+ nudgeGoalId?: string;
109
+ nudgeTargetKind?: UltragoalNudgeTargetKind;
70
110
  }
71
111
  export interface UltragoalCommandResult {
72
112
  reviewBlockerGoalIds?: string[];
@@ -82,6 +122,46 @@ interface JsonObject {
82
122
  export declare function hashStructuredValue(value: unknown): string;
83
123
  export declare function getUltragoalPaths(cwd: string, sessionId?: string | null): UltragoalPaths;
84
124
  export declare function readUltragoalLedger(cwd: string, sessionId?: string | null): Promise<UltragoalLedgerEvent[]>;
125
+ export declare const DEFAULT_ULTRAGOAL_NUDGE_BUDGET = 10;
126
+ /** Pure: count ledger `nudge` rows for an exact goalId. */
127
+ export declare function countUltragoalNudges(ledger: readonly UltragoalLedgerEvent[], goalId: string): number;
128
+ /**
129
+ * Resolve the per-story nudge budget. Project `./.skc/settings.json` overrides the
130
+ * user settings (`$SKC_CONFIG_DIR/settings.json` or `~/.skc/settings.json`), else the
131
+ * default. Mirrors the `skc.deepInterview.ambiguityThreshold` user+project precedence.
132
+ */
133
+ export declare function resolveUltragoalNudgeBudget(cwd: string): Promise<{
134
+ budget: number;
135
+ source: string;
136
+ }>;
137
+ /**
138
+ * Pure canonical selector shared by guards and status so `nudgeGoalId` can never
139
+ * diverge between what a guard consumes and what status displays. Prefers the active
140
+ * current-goal objective, then active > pending > failed (matching `chooseNextGoal`),
141
+ * then the aggregate final-receipt target when all stories are complete but the
142
+ * aggregate run still needs a final receipt. Returns null for verified-complete or
143
+ * absent/unrelated plans.
144
+ */
145
+ export declare function selectUltragoalNudgeTarget(plan: UltragoalPlan, options?: {
146
+ currentGoalObjective?: string;
147
+ retryFailed?: boolean;
148
+ }): UltragoalNudgeTarget | null;
149
+ /**
150
+ * Atomic consuming writer. Locks the ledger path, rereads + counts nudge rows for the
151
+ * target story, and appends exactly one `nudge` row inside the same critical section
152
+ * only while budget remains. Reuses the lockless `appendLedger` inside the lock (it
153
+ * does not acquire a conflicting lock), so concurrent guarded attempts cannot both
154
+ * observe `count = budget - 1` and overshoot the budget.
155
+ */
156
+ export declare function recordUltragoalNudgeIfBudgetRemaining(input: {
157
+ cwd: string;
158
+ sessionId?: string | null;
159
+ target: UltragoalNudgeTarget;
160
+ surface: UltragoalNudgeSurface;
161
+ budget: number;
162
+ reason: string;
163
+ currentGoalObjective?: string;
164
+ }): Promise<UltragoalNudgeOutcome>;
85
165
  export declare function computeUltragoalPlanGeneration(input: {
86
166
  plan: UltragoalPlan;
87
167
  ledger: readonly UltragoalLedgerEvent[];
@@ -28,6 +28,10 @@ export interface TabSession {
28
28
  kindTag: BrowserKindTag;
29
29
  /** Session that acquired this tab; used for session-scoped teardown (F13). */
30
30
  ownerId?: string;
31
+ /** Unix-ms timestamp of the last acquire/run activity; drives GC idle + LRU ordering. */
32
+ lastUsedAt: number;
33
+ /** Set synchronously by the shared begin-release guard so concurrent release is a no-op. */
34
+ releasing?: boolean;
31
35
  }
32
36
  export interface AcquireTabOptions {
33
37
  url?: string;
@@ -58,6 +62,33 @@ export interface ReleaseTabOptions {
58
62
  kill?: boolean;
59
63
  }
60
64
  export declare function getTab(name: string): TabSession | undefined;
65
+ /** Read-only, GC-facing projection of a live tab. Never exposes the mutable `tabs` map. */
66
+ export interface TabGcSnapshot {
67
+ name: string;
68
+ ownerId?: string;
69
+ state: TabSession["state"];
70
+ pendingCount: number;
71
+ kindTag: BrowserKindTag;
72
+ lastUsedAt: number;
73
+ browserRefCount: number;
74
+ }
75
+ export interface BrowserGcEligibilityPolicy {
76
+ now: () => number;
77
+ idleMs: number;
78
+ }
79
+ /** Snapshot every live tab for GC ordering/diagnostics. */
80
+ export declare function listTabsForGc(): TabGcSnapshot[];
81
+ /**
82
+ * Evict a tab only if it is still GC-eligible against the LIVE supervisor state. The full
83
+ * predicate is checked synchronously and `releaseTab` is invoked with no intervening await,
84
+ * so a tab that became busy after a GC snapshot cannot be closed (its run rejected). Returns
85
+ * true only when the tab was actually released.
86
+ */
87
+ export declare function releaseTabIfGcEligible(name: string, policy: BrowserGcEligibilityPolicy): Promise<boolean>;
88
+ /** Test-only: install a fabricated tab session. */
89
+ export declare function setTabForTest(tab: TabSession): void;
90
+ /** Test-only: clear the tab registry between cases. */
91
+ export declare function clearTabsForTest(): void;
61
92
  export declare function acquireTab(name: string, browser: BrowserHandle, opts: AcquireTabOptions): Promise<AcquireTabResult>;
62
93
  export declare function runInTab(name: string, opts: RunInTabOptions): Promise<RunResultOk>;
63
94
  export declare function releaseTab(name: string, opts?: ReleaseTabOptions): Promise<boolean>;
@@ -0,0 +1,23 @@
1
+ /** Prefix of every computer-use screenshot fallback directory created under the OS temp dir. */
2
+ export declare const SCREENSHOT_FALLBACK_DIR_PREFIX = "skc-computer-screenshots-";
3
+ /** Lazy-arm marker: called when the first screenshot fallback dir is created this process. */
4
+ export declare function markScreenshotFallbackDirCreatedForGc(): void;
5
+ /** Whether any screenshot fallback dir has been created this process (GC lazy-arm gate). */
6
+ export declare function hasCreatedScreenshotFallbackDir(): boolean;
7
+ /** Test-only: reset the lazy-arm marker between cases. */
8
+ export declare function resetScreenshotFallbackGcForTest(): void;
9
+ export interface ScreenshotGcOptions {
10
+ now: () => number;
11
+ staleMs: number;
12
+ /** Base dir to scan; defaults to os.tmpdir(). Injectable for tests. */
13
+ tmpDir?: string;
14
+ }
15
+ /**
16
+ * Disk-only GC for stale computer-use screenshot fallback directories. Scans the temp dir for
17
+ * `skc-computer-screenshots-*` directories and removes those whose mtime is older than `staleMs`.
18
+ * Never throws on a per-directory failure; the whole sweep is best-effort.
19
+ */
20
+ export declare function cleanupStaleScreenshotFallbackDirs(options: ScreenshotGcOptions): Promise<{
21
+ scanned: number;
22
+ removed: number;
23
+ }>;
@@ -5,18 +5,18 @@ import type { ToolSession } from "./index";
5
5
  export declare const MAX_CRON_TASKS_PER_OWNER = 50;
6
6
  /** Recurring tasks auto-expire 7 days after creation (mirrors upstream). */
7
7
  export declare const CRON_RECURRING_MAX_AGE_MS: number;
8
- declare const cronCreateSchema: z.ZodObject<{
9
- cron_expression: z.ZodString;
10
- prompt: z.ZodString;
11
- recurring: z.ZodDefault<z.ZodBoolean>;
12
- }, z.core.$strip>;
13
- export type CronCreateParams = z.infer<typeof cronCreateSchema>;
14
- declare const cronListSchema: z.ZodObject<{}, z.core.$strip>;
15
- export type CronListParams = z.infer<typeof cronListSchema>;
16
- declare const cronDeleteSchema: z.ZodObject<{
17
- id: z.ZodString;
18
- }, z.core.$strip>;
19
- export type CronDeleteParams = z.infer<typeof cronDeleteSchema>;
8
+ declare const cronSchema: z.ZodObject<{
9
+ op: z.ZodEnum<{
10
+ create: "create";
11
+ delete: "delete";
12
+ list: "list";
13
+ }>;
14
+ cron_expression: z.ZodOptional<z.ZodString>;
15
+ prompt: z.ZodOptional<z.ZodString>;
16
+ recurring: z.ZodOptional<z.ZodBoolean>;
17
+ id: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strict>;
19
+ export type CronParams = z.infer<typeof cronSchema>;
20
20
  export interface CronJobSnapshot {
21
21
  id: string;
22
22
  cron_expression: string;
@@ -35,18 +35,14 @@ export interface CronListJobDetails {
35
35
  prompt: string;
36
36
  humanSchedule: string;
37
37
  }
38
- export interface CronCreateToolDetails {
39
- id: string;
40
- cron_expression: string;
41
- recurring: boolean;
38
+ export interface CronToolDetails {
39
+ op: "create" | "list" | "delete";
40
+ id?: string;
41
+ cron_expression?: string;
42
+ recurring?: boolean;
42
43
  nextFireAt?: number;
43
- }
44
- export interface CronListToolDetails {
45
- jobs: CronListJobDetails[];
46
- }
47
- export interface CronDeleteToolDetails {
48
- id: string;
49
- deleted: boolean;
44
+ jobs?: CronListJobDetails[];
45
+ deleted?: boolean;
50
46
  }
51
47
  /** Clear every schedule for an owner. Exported for tests + lifecycle teardown. */
52
48
  export declare function clearOwnerSchedules(ownerId: string | undefined): void;
@@ -68,49 +64,28 @@ export declare function calculateCronFireTimeMs(params: {
68
64
  nowMs: number;
69
65
  expiresAt?: number;
70
66
  }): number;
71
- export declare class CronCreateTool implements AgentTool<typeof cronCreateSchema, CronCreateToolDetails> {
72
- private readonly session;
73
- readonly name = "CronCreate";
74
- readonly label = "CronCreate";
75
- readonly summary = "Schedule a prompt on a 5-field cron expression";
76
- readonly description: string;
77
- readonly parameters: z.ZodObject<{
78
- cron_expression: z.ZodString;
79
- prompt: z.ZodString;
80
- recurring: z.ZodDefault<z.ZodBoolean>;
81
- }, z.core.$strip>;
82
- readonly strict = true;
83
- readonly loadMode = "discoverable";
84
- constructor(session: ToolSession);
85
- static createIf(session: ToolSession): CronCreateTool | null;
86
- execute(_toolCallId: string, params: CronCreateParams, _signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<CronCreateToolDetails>, _context?: AgentToolContext): Promise<AgentToolResult<CronCreateToolDetails>>;
87
- }
88
- export declare class CronListTool implements AgentTool<typeof cronListSchema, CronListToolDetails> {
89
- private readonly session;
90
- readonly name = "CronList";
91
- readonly label = "CronList";
92
- readonly summary = "List scheduled cron jobs";
93
- readonly description: string;
94
- readonly parameters: z.ZodObject<{}, z.core.$strip>;
95
- readonly strict = true;
96
- readonly loadMode = "discoverable";
97
- constructor(session: ToolSession);
98
- static createIf(session: ToolSession): CronListTool | null;
99
- execute(_toolCallId: string, _params: CronListParams, _signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<CronListToolDetails>, _context?: AgentToolContext): Promise<AgentToolResult<CronListToolDetails>>;
100
- }
101
- export declare class CronDeleteTool implements AgentTool<typeof cronDeleteSchema, CronDeleteToolDetails> {
67
+ export declare class CronTool implements AgentTool<typeof cronSchema, CronToolDetails> {
68
+ #private;
102
69
  private readonly session;
103
- readonly name = "CronDelete";
104
- readonly label = "CronDelete";
105
- readonly summary = "Cancel a scheduled cron job by ID";
70
+ readonly name = "cron";
71
+ readonly label = "Cron";
72
+ readonly summary = "Schedule, list, and cancel cron-style prompts (op: create | list | delete)";
106
73
  readonly description: string;
107
74
  readonly parameters: z.ZodObject<{
108
- id: z.ZodString;
109
- }, z.core.$strip>;
75
+ op: z.ZodEnum<{
76
+ create: "create";
77
+ delete: "delete";
78
+ list: "list";
79
+ }>;
80
+ cron_expression: z.ZodOptional<z.ZodString>;
81
+ prompt: z.ZodOptional<z.ZodString>;
82
+ recurring: z.ZodOptional<z.ZodBoolean>;
83
+ id: z.ZodOptional<z.ZodString>;
84
+ }, z.core.$strict>;
110
85
  readonly strict = true;
111
86
  readonly loadMode = "discoverable";
112
87
  constructor(session: ToolSession);
113
- static createIf(session: ToolSession): CronDeleteTool | null;
114
- execute(_toolCallId: string, params: CronDeleteParams, _signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<CronDeleteToolDetails>, _context?: AgentToolContext): Promise<AgentToolResult<CronDeleteToolDetails>>;
88
+ static createIf(session: ToolSession): CronTool | null;
89
+ execute(_toolCallId: string, params: CronParams, _signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<CronToolDetails>, _context?: AgentToolContext): Promise<AgentToolResult<CronToolDetails>>;
115
90
  }
116
91
  export {};
@@ -41,7 +41,6 @@ export * from "./eval";
41
41
  export * from "./find";
42
42
  export * from "./gh";
43
43
  export * from "./image-gen";
44
- export * from "./inspect-image";
45
44
  export * from "./irc";
46
45
  export * from "./job";
47
46
  export * from "./monitor";
@@ -0,0 +1,54 @@
1
+ import type { Settings } from "../config/settings";
2
+ import { type TabGcSnapshot } from "./browser/tab-supervisor";
3
+ export interface BrowserGcPolicy {
4
+ enabled: boolean;
5
+ idleMs: number;
6
+ rssLimitBytes: number;
7
+ }
8
+ export interface ComputerGcPolicy {
9
+ enabled: boolean;
10
+ staleMs: number;
11
+ scanIntervalMs: number;
12
+ }
13
+ export declare function resolveBrowserGcPolicy(settings: Settings): BrowserGcPolicy;
14
+ export declare function resolveComputerGcPolicy(settings: Settings): ComputerGcPolicy;
15
+ export declare function resolveSweepIntervalMs(settings: Settings): number;
16
+ /** Injectable seams so the controller is fully testable without real browsers/filesystem/RSS. */
17
+ export interface ResourceGcDeps {
18
+ now: () => number;
19
+ rssBytes: () => number;
20
+ logWarn: (msg: string, meta?: Record<string, unknown>) => void;
21
+ listTabs: () => TabGcSnapshot[];
22
+ releaseTab: (name: string, policy: {
23
+ now: () => number;
24
+ idleMs: number;
25
+ }) => Promise<boolean>;
26
+ cleanupScreenshots: (opts: {
27
+ now: () => number;
28
+ staleMs: number;
29
+ }) => Promise<{
30
+ scanned: number;
31
+ removed: number;
32
+ }>;
33
+ screenshotArmed: () => boolean;
34
+ }
35
+ export interface ResourceGcRegistration {
36
+ sessionId: string;
37
+ settings: Settings;
38
+ }
39
+ /**
40
+ * Register a session with the resource GC. Starts the single shared timer on the first
41
+ * registration. Returns an idempotent unregister function; the timer stops only when the last
42
+ * session unregisters.
43
+ */
44
+ export declare function registerResourceGcSession(reg: ResourceGcRegistration): () => void;
45
+ export declare function sweepOnce(d?: ResourceGcDeps): Promise<void>;
46
+ export declare function __setResourceGcDepsForTest(overrides: Partial<ResourceGcDeps>): void;
47
+ export declare function __runResourceGcTickForTest(): Promise<void>;
48
+ export declare function __getResourceGcStateForTest(): {
49
+ timerActive: boolean;
50
+ sessionCount: number;
51
+ rssWarningActive: boolean;
52
+ inProgress: boolean;
53
+ };
54
+ export declare function __resetResourceGcForTest(): void;
@@ -111,5 +111,6 @@ export declare class WebSearchTool implements AgentTool<typeof webSearchSchema,
111
111
  export declare const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRenderDetails>;
112
112
  export declare function getSearchTools(): CustomTool<any, any>[];
113
113
  export { getSearchProvider, setPreferredSearchProvider, setSearchFallbackProviders } from "./provider";
114
+ export { setSearchHardTimeoutMs } from "./providers/utils";
114
115
  export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
115
116
  export { isConfigurableSearchProviderId, isSearchProviderPreference } from "./types";
@@ -15,16 +15,23 @@ import { SearchProviderError, type SearchProviderId, type SearchSource } from ".
15
15
  */
16
16
  export declare function findCredential(storage: AgentStorage | null | undefined, envKey: string | null | undefined, ...storageProviders: string[]): string | null;
17
17
  /**
18
- * Default hard ceiling for a single web-search round-trip. 60s tolerates
18
+ * Default hard ceiling for a single web-search round-trip. 300s tolerates
19
19
  * legitimate slow LLM-mediated responses (anthropic web_search_20250305,
20
20
  * perplexity, gemini, OpenAI code backend) while still guaranteeing the session unfreezes
21
- * within a minute if Bun's `AbortSignal` fails to propagate on Windows.
21
+ * if Bun's `AbortSignal` fails to propagate on Windows.
22
22
  *
23
23
  * Pure search APIs (brave, exa, jina, tavily, searxng, synthetic, zai)
24
24
  * settle far faster in practice; reusing the same ceiling keeps the wiring
25
25
  * uniform without compromising correctness.
26
26
  */
27
- export declare const SEARCH_HARD_TIMEOUT_MS = 60000;
27
+ export declare const SEARCH_HARD_TIMEOUT_MS = 300000;
28
+ /**
29
+ * Override the hard timeout applied to every web-search round-trip.
30
+ *
31
+ * @param ms - Hard timeout in milliseconds. Non-finite or non-positive
32
+ * values reset the timeout to {@link SEARCH_HARD_TIMEOUT_MS}.
33
+ */
34
+ export declare function setSearchHardTimeoutMs(ms: number | undefined): void;
28
35
  /**
29
36
  * Compose a caller-supplied {@link AbortSignal} with a hard timeout so an
30
37
  * outbound `fetch()` is guaranteed to settle within `ms` even when the
@@ -36,7 +43,7 @@ export declare const SEARCH_HARD_TIMEOUT_MS = 60000;
36
43
  * because the user's Esc is never delivered to the native layer.
37
44
  *
38
45
  * @param signal - Caller cancellation signal, if any.
39
- * @param ms - Hard timeout in milliseconds. Defaults to {@link SEARCH_HARD_TIMEOUT_MS}.
46
+ * @param ms - Hard timeout in milliseconds. Defaults to the configured value.
40
47
  */
41
48
  export declare function withHardTimeout(signal: AbortSignal | undefined, ms?: number): AbortSignal;
42
49
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/coding-agent",
4
- "version": "0.3.2",
4
+ "version": "0.3.5",
5
5
  "description": "Sayknow-CLI CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/jaybeyond/Sayknow_CLI",
7
7
  "author": "jaybeyond",
@@ -52,12 +52,12 @@
52
52
  "@agentclientprotocol/sdk": "0.21.0",
53
53
  "@babel/parser": "^7.29.3",
54
54
  "@mozilla/readability": "^0.6.0",
55
- "@sayknow-cli/stats": "0.3.2",
56
- "@sayknow-cli/agent-core": "0.3.2",
57
- "@sayknow-cli/ai": "0.3.2",
58
- "@sayknow-cli/natives": "0.3.2",
59
- "@sayknow-cli/tui": "0.3.2",
60
- "@sayknow-cli/utils": "0.3.2",
55
+ "@sayknow-cli/stats": "0.3.5",
56
+ "@sayknow-cli/agent-core": "0.3.5",
57
+ "@sayknow-cli/ai": "0.3.5",
58
+ "@sayknow-cli/natives": "0.3.5",
59
+ "@sayknow-cli/tui": "0.3.5",
60
+ "@sayknow-cli/utils": "0.3.5",
61
61
  "@puppeteer/browsers": "^2.13.0",
62
62
  "@types/turndown": "5.0.6",
63
63
  "@xterm/headless": "^6.0.0",
@@ -28,6 +28,11 @@ function fail(msg: string): void {
28
28
  function walk(dir: string): string[] {
29
29
  const out: string[] = [];
30
30
  for (const entry of readdirSync(dir)) {
31
+ // node_modules holds locally-installed Phase 3 runtime deps (gitignored,
32
+ // never vendored and never packed by npm). Vendor checks apply only to
33
+ // vendored upstream source; third-party package files legitimately carry
34
+ // paths like ".../references/" that would otherwise false-positive.
35
+ if (entry === "node_modules") continue;
31
36
  const full = join(dir, entry);
32
37
  if (statSync(full).isDirectory()) out.push(...walk(full));
33
38
  else out.push(full);
package/src/cli/args.ts CHANGED
@@ -284,7 +284,6 @@ ${chalk.bold("Available Tools (default-enabled unless noted):")}
284
284
  lsp - Language server protocol (code intelligence)
285
285
  python - Execute Python code (requires: ${APP_NAME} setup python)
286
286
  notebook - Edit Jupyter notebooks
287
- inspect_image - Analyze images with a vision model
288
287
  browser - Browser automation (Puppeteer)
289
288
  task - Launch sub-agents for parallel tasks
290
289
  todo_write - Manage todo/task lists
@@ -97,7 +97,6 @@ Available Tools (default-enabled unless noted):
97
97
  lsp - Language server protocol (code intelligence)
98
98
  python - Execute Python code (requires: ${APP_NAME} setup python)
99
99
  notebook - Edit Jupyter notebooks
100
- inspect_image - Analyze images with a vision model
101
100
  browser - Browser automation (Puppeteer)
102
101
  task - Launch sub-agents for parallel tasks
103
102
  todo_write - Manage todo/task lists
@@ -16,8 +16,8 @@ import {
16
16
  getPluginsCacheDir,
17
17
  MarketplaceManager,
18
18
  } from "../extensibility/plugins/marketplace/index.js";
19
- import { installSkcPluginBundle, isSkcPluginBundleSource, readRegistry } from "../extensibility/skc-plugins";
20
19
  import { type ScanReport, scanPluginDir } from "../extensibility/plugins/security-scanner";
20
+ import { installSkcPluginBundle, isSkcPluginBundleSource, readRegistry } from "../extensibility/skc-plugins";
21
21
  import { theme } from "../modes/theme/theme";
22
22
 
23
23
  // =============================================================================
@@ -15,6 +15,7 @@ import {
15
15
  type SearchQueryParams,
16
16
  } from "../web/search/index";
17
17
  import { SEARCH_PROVIDER_ORDER, setPreferredSearchProvider, setSearchFallbackProviders } from "../web/search/provider";
18
+ import { setSearchHardTimeoutMs } from "../web/search/providers/utils";
18
19
  import { renderSearchResult } from "../web/search/render";
19
20
  import type { SearchProviderId } from "../web/search/types";
20
21
 
@@ -179,6 +180,10 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
179
180
  configuredFallback.filter(value => typeof value === "string" && isConfigurableSearchProviderId(value)),
180
181
  );
181
182
  }
183
+ const configuredTimeout = settings.get("web_search.timeout");
184
+ if (typeof configuredTimeout === "number" && Number.isFinite(configuredTimeout) && configuredTimeout > 0) {
185
+ setSearchHardTimeoutMs(configuredTimeout * 1000);
186
+ }
182
187
 
183
188
  const params: SearchQueryParams = {
184
189
  query: cmd.query,
@@ -9,6 +9,7 @@ import {
9
9
  } from "./model-profiles";
10
10
  import {
11
11
  isAuthenticated,
12
+ kNoAuth,
12
13
  type ModelRegistry,
13
14
  SKC_MODEL_ASSIGNMENT_TARGETS,
14
15
  type SkcModelAssignmentTargetId,
@@ -192,7 +193,12 @@ export async function prepareModelProfileActivation(
192
193
  const authenticatedProviders: string[] = [];
193
194
  for (const provider of allProviders) {
194
195
  const apiKey = await options.modelRegistry.getApiKeyForProvider(provider, options.session.sessionId);
195
- if (!isAuthenticated(apiKey)) {
196
+ // kNoAuth ("N/A") is the sentinel for keyless/no-auth providers (local LLMs,
197
+ // `--auth none` custom providers): usable WITHOUT a key. isAuthenticated()
198
+ // rejects it, so gate on usability ("a credential resolved OR keyless")
199
+ // rather than "has a real key" — otherwise activating a keyless profile
200
+ // throws a spurious "requires credentials" error and the model never applies.
201
+ if (!isAuthenticated(apiKey) && apiKey !== kNoAuth) {
196
202
  missingProviders.push(provider);
197
203
  } else {
198
204
  authenticatedProviders.push(provider);
@@ -23,8 +23,8 @@ export interface ModelProfileDefinition {
23
23
 
24
24
  export interface ResolvedProfileBinding {
25
25
  defaultSelector?: string;
26
- modelRoles: Partial<Record<"vision", string>>;
27
- agentModelOverrides: Partial<Record<Exclude<ModelProfileRole, "default" | "vision">, string>>;
26
+ modelRoles: Record<string, string>;
27
+ agentModelOverrides: Partial<Record<Exclude<ModelProfileRole, "default">, string>>;
28
28
  }
29
29
 
30
30
  function parseModelSelectorProvider(selector: string): string | undefined {
@@ -383,9 +383,8 @@ export function mergeModelProfiles(userProfiles?: ModelsConfig["profiles"]): Map
383
383
  }
384
384
 
385
385
  export function resolveProfileBindings(definition: ModelProfileDefinition): ResolvedProfileBinding {
386
- const { default: defaultSelector, vision, executor, architect, planner, critic } = definition.modelMapping;
386
+ const { default: defaultSelector, executor, architect, planner, critic } = definition.modelMapping;
387
387
  const modelRoles: ResolvedProfileBinding["modelRoles"] = {};
388
- if (vision !== undefined) modelRoles.vision = vision;
389
388
  const agentModelOverrides: ResolvedProfileBinding["agentModelOverrides"] = {};
390
389
  if (executor !== undefined) agentModelOverrides.executor = executor;
391
390
  if (architect !== undefined) agentModelOverrides.architect = architect;
@@ -70,7 +70,7 @@ export function isAuthenticated(apiKey: string | undefined | null): apiKey is st
70
70
  return Boolean(apiKey) && apiKey !== kNoAuth;
71
71
  }
72
72
 
73
- export type ModelRole = "default" | "vision";
73
+ export type ModelRole = "default";
74
74
 
75
75
  export interface ModelRoleInfo {
76
76
  tag?: string;
@@ -80,12 +80,11 @@ export interface ModelRoleInfo {
80
80
 
81
81
  export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
82
82
  default: { tag: "DEFAULT", name: "Default", color: "success" },
83
- vision: { tag: "VISION", name: "Vision", color: "accent" },
84
83
  };
85
84
 
86
- export const MODEL_ROLE_IDS: ModelRole[] = ["default", "vision"];
85
+ export const MODEL_ROLE_IDS: ModelRole[] = ["default"];
87
86
 
88
- export type SkcModelAssignmentTargetId = "default" | "vision" | "executor" | "architect" | "planner" | "critic";
87
+ export type SkcModelAssignmentTargetId = "default" | "executor" | "architect" | "planner" | "critic";
89
88
 
90
89
  export interface SkcModelAssignmentTargetInfo extends ModelRoleInfo {
91
90
  id: SkcModelAssignmentTargetId;
@@ -94,7 +93,6 @@ export interface SkcModelAssignmentTargetInfo extends ModelRoleInfo {
94
93
 
95
94
  export const SKC_MODEL_ASSIGNMENT_TARGET_IDS: SkcModelAssignmentTargetId[] = [
96
95
  "default",
97
- "vision",
98
96
  "executor",
99
97
  "architect",
100
98
  "planner",
@@ -103,7 +101,6 @@ export const SKC_MODEL_ASSIGNMENT_TARGET_IDS: SkcModelAssignmentTargetId[] = [
103
101
 
104
102
  export const SKC_MODEL_ASSIGNMENT_TARGETS: Record<SkcModelAssignmentTargetId, SkcModelAssignmentTargetInfo> = {
105
103
  default: { id: "default", tag: "DEFAULT", name: "Default", color: "success", settingsPath: "modelRoles" },
106
- vision: { id: "vision", tag: "VISION", name: "Vision", color: "accent", settingsPath: "modelRoles" },
107
104
  executor: {
108
105
  id: "executor",
109
106
  tag: "EXECUTOR",
@@ -81,7 +81,7 @@ const ModelBindingsSchema = z.object({
81
81
  modelRoles: z.record(z.string(), z.string().min(1)).optional(),
82
82
  agentModelOverrides: z.record(z.string(), z.string().min(1)).optional(),
83
83
  });
84
- export const ProfileRoleSchema = z.enum(["default", "vision", "executor", "architect", "planner", "critic"]);
84
+ export const ProfileRoleSchema = z.enum(["default", "executor", "architect", "planner", "critic"]);
85
85
 
86
86
  function isValidProfileModelSelector(value: string): boolean {
87
87
  if (value.includes(",")) return false;