@gajae-code/coding-agent 0.5.4 → 0.6.1

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 (155) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/cli/web-search-cli.d.ts +12 -0
  3. package/dist/types/commands/rlm.d.ts +10 -0
  4. package/dist/types/commands/web-search.d.ts +54 -0
  5. package/dist/types/config/keybindings.d.ts +10 -0
  6. package/dist/types/config/model-profiles.d.ts +2 -1
  7. package/dist/types/config/model-registry.d.ts +3 -0
  8. package/dist/types/config/models-config-schema.d.ts +3 -0
  9. package/dist/types/config/settings-schema.d.ts +61 -3
  10. package/dist/types/edit/notebook.d.ts +3 -0
  11. package/dist/types/eval/py/executor.d.ts +3 -0
  12. package/dist/types/eval/py/kernel.d.ts +3 -1
  13. package/dist/types/eval/py/runtime.d.ts +9 -1
  14. package/dist/types/exec/bash-executor.d.ts +4 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  16. package/dist/types/extensibility/custom-tools/wrapper.d.ts +1 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  18. package/dist/types/extensibility/extensions/wrapper.d.ts +1 -0
  19. package/dist/types/gjc-runtime/launch-tmux.d.ts +6 -0
  20. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +14 -0
  21. package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
  22. package/dist/types/gjc-runtime/tmux-gc.d.ts +3 -3
  23. package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -0
  24. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +18 -0
  25. package/dist/types/goals/state.d.ts +1 -1
  26. package/dist/types/goals/tools/goal-tool.d.ts +2 -0
  27. package/dist/types/main.d.ts +11 -0
  28. package/dist/types/modes/components/custom-editor.d.ts +4 -2
  29. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +12 -0
  30. package/dist/types/modes/components/model-selector.d.ts +5 -2
  31. package/dist/types/modes/components/status-line.d.ts +4 -1
  32. package/dist/types/modes/controllers/input-controller.d.ts +3 -0
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  34. package/dist/types/modes/print-mode.d.ts +6 -0
  35. package/dist/types/modes/rpc/rpc-client.d.ts +21 -0
  36. package/dist/types/modes/rpc/rpc-socket-security.d.ts +7 -0
  37. package/dist/types/modes/rpc/rpc-types.d.ts +13 -0
  38. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +2 -0
  39. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +1 -0
  40. package/dist/types/rlm/artifacts.d.ts +9 -0
  41. package/dist/types/rlm/complete-research-tool.d.ts +35 -0
  42. package/dist/types/rlm/data-context.d.ts +6 -0
  43. package/dist/types/rlm/index.d.ts +35 -0
  44. package/dist/types/rlm/notebook.d.ts +12 -0
  45. package/dist/types/rlm/preset.d.ts +23 -0
  46. package/dist/types/rlm/python-tool.d.ts +16 -0
  47. package/dist/types/rlm/report.d.ts +14 -0
  48. package/dist/types/rlm/types.d.ts +37 -0
  49. package/dist/types/sdk.d.ts +7 -0
  50. package/dist/types/session/agent-session.d.ts +21 -0
  51. package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -1
  52. package/dist/types/tools/browser/attach.d.ts +19 -3
  53. package/dist/types/tools/browser/registry.d.ts +15 -0
  54. package/dist/types/tools/browser/render.d.ts +3 -0
  55. package/dist/types/tools/browser.d.ts +18 -1
  56. package/dist/types/tools/computer/render.d.ts +17 -0
  57. package/dist/types/tools/computer.d.ts +465 -0
  58. package/dist/types/tools/index.d.ts +24 -1
  59. package/dist/types/tools/job.d.ts +13 -0
  60. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  61. package/dist/types/web/search/index.d.ts +32 -2
  62. package/dist/types/web/search/providers/base.d.ts +22 -0
  63. package/dist/types/web/search/providers/xai.d.ts +64 -0
  64. package/dist/types/web/search/types.d.ts +11 -3
  65. package/package.json +7 -7
  66. package/src/cli/web-search-cli.ts +123 -8
  67. package/src/cli.ts +2 -0
  68. package/src/commands/rlm.ts +19 -0
  69. package/src/commands/web-search.ts +66 -0
  70. package/src/config/keybindings.ts +11 -0
  71. package/src/config/model-profiles.ts +11 -3
  72. package/src/config/model-registry.ts +55 -1
  73. package/src/config/models-config-schema.ts +1 -0
  74. package/src/config/settings-schema.ts +67 -1
  75. package/src/edit/notebook.ts +6 -2
  76. package/src/eval/py/executor.ts +8 -1
  77. package/src/eval/py/kernel.ts +9 -4
  78. package/src/eval/py/runtime.ts +153 -32
  79. package/src/exec/bash-executor.ts +10 -4
  80. package/src/extensibility/custom-tools/types.ts +2 -0
  81. package/src/extensibility/custom-tools/wrapper.ts +2 -0
  82. package/src/extensibility/extensions/types.ts +2 -0
  83. package/src/extensibility/extensions/wrapper.ts +1 -0
  84. package/src/gjc-runtime/launch-tmux.ts +129 -1
  85. package/src/gjc-runtime/session-state-sidecar.ts +61 -1
  86. package/src/gjc-runtime/tmux-common.ts +26 -2
  87. package/src/gjc-runtime/tmux-gc.ts +40 -27
  88. package/src/gjc-runtime/tmux-sessions.ts +13 -1
  89. package/src/gjc-runtime/ultragoal-runtime.ts +340 -18
  90. package/src/goals/runtime.ts +4 -3
  91. package/src/goals/state.ts +1 -1
  92. package/src/goals/tools/goal-tool.ts +16 -3
  93. package/src/internal-urls/docs-index.generated.ts +13 -9
  94. package/src/main.ts +28 -3
  95. package/src/modes/components/custom-editor.ts +13 -4
  96. package/src/modes/components/custom-model-preset-wizard.ts +293 -0
  97. package/src/modes/components/hook-selector.ts +1 -1
  98. package/src/modes/components/model-selector.ts +72 -29
  99. package/src/modes/components/skill-message.ts +62 -8
  100. package/src/modes/components/status-line.ts +13 -1
  101. package/src/modes/controllers/input-controller.ts +60 -11
  102. package/src/modes/controllers/selector-controller.ts +39 -0
  103. package/src/modes/interactive-mode.ts +1 -1
  104. package/src/modes/print-mode.ts +14 -4
  105. package/src/modes/rpc/rpc-client.ts +250 -80
  106. package/src/modes/rpc/rpc-mode.ts +6 -12
  107. package/src/modes/rpc/rpc-socket-security.ts +103 -0
  108. package/src/modes/rpc/rpc-types.ts +10 -0
  109. package/src/modes/shared/agent-wire/command-dispatch.ts +7 -0
  110. package/src/modes/shared/agent-wire/command-validation.ts +1 -0
  111. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  112. package/src/modes/shared/agent-wire/unattended-session.ts +9 -0
  113. package/src/modes/utils/hotkeys-markdown.ts +4 -2
  114. package/src/modes/utils/ui-helpers.ts +2 -2
  115. package/src/prompts/goals/goal-continuation.md +1 -0
  116. package/src/prompts/goals/goal-mode-active.md +1 -0
  117. package/src/prompts/system/rlm-report-command.md +1 -0
  118. package/src/prompts/system/rlm-research.md +23 -0
  119. package/src/prompts/tools/bash.md +23 -2
  120. package/src/prompts/tools/browser.md +7 -3
  121. package/src/prompts/tools/computer.md +74 -0
  122. package/src/prompts/tools/goal.md +3 -0
  123. package/src/prompts/tools/job.md +9 -1
  124. package/src/prompts/tools/web-search.md +7 -0
  125. package/src/rlm/artifacts.ts +60 -0
  126. package/src/rlm/complete-research-tool.ts +163 -0
  127. package/src/rlm/data-context.ts +26 -0
  128. package/src/rlm/index.ts +339 -0
  129. package/src/rlm/notebook.ts +108 -0
  130. package/src/rlm/preset.ts +76 -0
  131. package/src/rlm/python-tool.ts +68 -0
  132. package/src/rlm/report.ts +70 -0
  133. package/src/rlm/types.ts +40 -0
  134. package/src/sdk.ts +12 -0
  135. package/src/session/agent-session.ts +48 -3
  136. package/src/slash-commands/builtin-registry.ts +17 -0
  137. package/src/tools/bash-allowed-prefixes.ts +84 -1
  138. package/src/tools/bash.ts +80 -13
  139. package/src/tools/browser/attach.ts +103 -3
  140. package/src/tools/browser/registry.ts +176 -2
  141. package/src/tools/browser/render.ts +9 -1
  142. package/src/tools/browser.ts +33 -0
  143. package/src/tools/computer/render.ts +78 -0
  144. package/src/tools/computer.ts +640 -0
  145. package/src/tools/index.ts +41 -1
  146. package/src/tools/job.ts +88 -5
  147. package/src/tools/json-tree.ts +42 -29
  148. package/src/tools/renderers.ts +2 -0
  149. package/src/tools/tool-timeouts.ts +1 -0
  150. package/src/web/search/index.ts +27 -2
  151. package/src/web/search/provider.ts +16 -1
  152. package/src/web/search/providers/base.ts +22 -0
  153. package/src/web/search/providers/xai.ts +511 -0
  154. package/src/web/search/render.ts +7 -0
  155. package/src/web/search/types.ts +11 -1
@@ -32,6 +32,28 @@ export interface SearchParams {
32
32
  maxOutputTokens?: number;
33
33
  numSearchResults?: number;
34
34
  temperature?: number;
35
+ /** xAI-specific search surface. Defaults to web_search when omitted. */
36
+ xaiSearchMode?: "web" | "x" | "web_and_x";
37
+ /** xAI web_search domain allow-list (max 5). */
38
+ allowedDomains?: string[];
39
+ /** xAI web_search domain deny-list (max 5). */
40
+ excludedDomains?: string[];
41
+ /** xAI x_search handle allow-list (max 20). */
42
+ allowedXHandles?: string[];
43
+ /** xAI x_search handle deny-list (max 20). */
44
+ excludedXHandles?: string[];
45
+ /** xAI x_search lower date bound, ISO8601 date such as YYYY-MM-DD. */
46
+ fromDate?: string;
47
+ /** xAI x_search upper date bound, ISO8601 date such as YYYY-MM-DD. */
48
+ toDate?: string;
49
+ /** xAI web_search/x_search image understanding. */
50
+ enableImageUnderstanding?: boolean;
51
+ /** xAI web_search image search result embedding. */
52
+ enableImageSearch?: boolean;
53
+ /** xAI x_search video understanding. */
54
+ enableVideoUnderstanding?: boolean;
55
+ /** xAI Responses include=["no_inline_citations"]. */
56
+ noInlineCitations?: boolean;
35
57
  googleSearch?: Record<string, unknown>;
36
58
  codeExecution?: Record<string, unknown>;
37
59
  urlContext?: Record<string, unknown>;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * xAI Web/X Search Provider
3
+ *
4
+ * Uses xAI's Responses API with the built-in web_search and x_search tools.
5
+ * Endpoint: POST https://api.x.ai/v1/responses
6
+ */
7
+ import type { AuthStorage } from "@gajae-code/ai";
8
+ import type { SearchCitation, SearchResponse } from "../../../web/search/types";
9
+ import type { SearchParams } from "./base";
10
+ import { SearchProvider } from "./base";
11
+ declare const XAI_SEARCH_MODES: readonly ["web", "x", "web_and_x"];
12
+ export type XaiSearchMode = (typeof XAI_SEARCH_MODES)[number];
13
+ export interface XaiSearchParams {
14
+ query: string;
15
+ system_prompt?: string;
16
+ num_results?: number;
17
+ max_output_tokens?: number;
18
+ temperature?: number;
19
+ recency?: "day" | "week" | "month" | "year";
20
+ xai_search_mode?: XaiSearchMode;
21
+ allowed_domains?: string[];
22
+ excluded_domains?: string[];
23
+ allowed_x_handles?: string[];
24
+ excluded_x_handles?: string[];
25
+ from_date?: string;
26
+ to_date?: string;
27
+ enable_image_understanding?: boolean;
28
+ enable_image_search?: boolean;
29
+ enable_video_understanding?: boolean;
30
+ no_inline_citations?: boolean;
31
+ signal?: AbortSignal;
32
+ authStorage: AuthStorage;
33
+ sessionId?: string;
34
+ }
35
+ export declare function buildXaiRequestBody(params: {
36
+ query: string;
37
+ systemPrompt: string;
38
+ model: string;
39
+ maxOutputTokens?: number;
40
+ temperature?: number;
41
+ recency?: XaiSearchParams["recency"];
42
+ xaiSearchMode?: XaiSearchMode;
43
+ allowedDomains?: string[];
44
+ excludedDomains?: string[];
45
+ allowedXHandles?: string[];
46
+ excludedXHandles?: string[];
47
+ fromDate?: string;
48
+ toDate?: string;
49
+ enableImageUnderstanding?: boolean;
50
+ enableImageSearch?: boolean;
51
+ enableVideoUnderstanding?: boolean;
52
+ noInlineCitations?: boolean;
53
+ }): Record<string, unknown>;
54
+ export declare function parseXaiCitations(json: any): SearchCitation[];
55
+ /** Execute xAI web/X search through the Responses API search tools. */
56
+ export declare function searchXai(params: XaiSearchParams): Promise<SearchResponse>;
57
+ /** Search provider for xAI web and X search. */
58
+ export declare class XaiProvider extends SearchProvider {
59
+ readonly id = "xai";
60
+ readonly label = "xAI";
61
+ isAvailable(authStorage: AuthStorage): boolean;
62
+ search(params: SearchParams): Promise<SearchResponse>;
63
+ }
64
+ export {};
@@ -4,7 +4,7 @@
4
4
  * Unified types for web search responses across supported providers.
5
5
  */
6
6
  /** Supported web search providers */
7
- export type SearchProviderId = "duckduckgo" | "exa" | "brave" | "jina" | "kimi" | "zai" | "anthropic" | "perplexity" | "gemini" | "codex" | "tavily" | "parallel" | "kagi" | "synthetic" | "searxng" | "openai-compatible";
7
+ export type SearchProviderId = "duckduckgo" | "exa" | "brave" | "jina" | "kimi" | "zai" | "anthropic" | "perplexity" | "gemini" | "codex" | "xai" | "tavily" | "parallel" | "kagi" | "synthetic" | "searxng" | "openai-compatible";
8
8
  export type WebSearchMode = "on" | "off" | "auto";
9
9
  export interface ActiveSearchModelContext {
10
10
  provider: string;
@@ -15,7 +15,7 @@ export interface ActiveSearchModelContext {
15
15
  headers?: Record<string, string>;
16
16
  webSearch?: WebSearchMode;
17
17
  }
18
- export declare const CONFIGURABLE_SEARCH_PROVIDER_IDS: readonly ["duckduckgo", "exa", "brave", "jina", "kimi", "zai", "anthropic", "perplexity", "gemini", "codex", "tavily", "parallel", "kagi", "synthetic", "searxng"];
18
+ export declare const CONFIGURABLE_SEARCH_PROVIDER_IDS: readonly ["duckduckgo", "exa", "brave", "jina", "kimi", "zai", "anthropic", "perplexity", "gemini", "codex", "xai", "tavily", "parallel", "kagi", "synthetic", "searxng"];
19
19
  export declare function isSearchProviderId(value: string): value is SearchProviderId;
20
20
  export declare function isConfigurableSearchProviderId(value: string): value is (typeof CONFIGURABLE_SEARCH_PROVIDER_IDS)[number];
21
21
  export declare function isSearchProviderPreference(value: string): value is (typeof CONFIGURABLE_SEARCH_PROVIDER_IDS)[number] | "auto";
@@ -40,8 +40,16 @@ export interface SearchCitation {
40
40
  export interface SearchUsage {
41
41
  inputTokens?: number;
42
42
  outputTokens?: number;
43
- /** Anthropic: number of web search requests made */
43
+ /** Number of web search requests made */
44
44
  searchRequests?: number;
45
+ /** Number of xAI X Search requests made */
46
+ xSearchRequests?: number;
47
+ /** Number of image search requests made */
48
+ imageSearchRequests?: number;
49
+ /** Number of image-understanding/view-image requests made */
50
+ imageUnderstandingRequests?: number;
51
+ /** Number of video-understanding requests made */
52
+ videoUnderstandingRequests?: number;
45
53
  /** Perplexity: combined token count */
46
54
  totalTokens?: number;
47
55
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.5.4",
4
+ "version": "0.6.1",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -51,12 +51,12 @@
51
51
  "@agentclientprotocol/sdk": "0.21.0",
52
52
  "@babel/parser": "^7.29.3",
53
53
  "@mozilla/readability": "^0.6.0",
54
- "@gajae-code/stats": "0.5.4",
55
- "@gajae-code/agent-core": "0.5.4",
56
- "@gajae-code/ai": "0.5.4",
57
- "@gajae-code/natives": "0.5.4",
58
- "@gajae-code/tui": "0.5.4",
59
- "@gajae-code/utils": "0.5.4",
54
+ "@gajae-code/stats": "0.6.1",
55
+ "@gajae-code/agent-core": "0.6.1",
56
+ "@gajae-code/ai": "0.6.1",
57
+ "@gajae-code/natives": "0.6.1",
58
+ "@gajae-code/tui": "0.6.1",
59
+ "@gajae-code/utils": "0.6.1",
60
60
  "@puppeteer/browsers": "^2.13.0",
61
61
  "@types/turndown": "5.0.6",
62
62
  "@xterm/headless": "^6.0.0",
@@ -6,9 +6,15 @@
6
6
 
7
7
  import { APP_NAME } from "@gajae-code/utils";
8
8
  import chalk from "chalk";
9
+ import { Settings } from "../config/settings";
9
10
  import { initTheme, theme } from "../modes/theme/theme";
10
- import { runSearchQuery, type SearchQueryParams } from "../web/search/index";
11
- import { SEARCH_PROVIDER_ORDER } from "../web/search/provider";
11
+ import {
12
+ isConfigurableSearchProviderId,
13
+ isSearchProviderPreference,
14
+ runSearchQuery,
15
+ type SearchQueryParams,
16
+ } from "../web/search/index";
17
+ import { SEARCH_PROVIDER_ORDER, setPreferredSearchProvider, setSearchFallbackProviders } from "../web/search/provider";
12
18
  import { renderSearchResult } from "../web/search/render";
13
19
  import type { SearchProviderId } from "../web/search/types";
14
20
 
@@ -18,11 +24,45 @@ export interface SearchCommandArgs {
18
24
  recency?: "day" | "week" | "month" | "year";
19
25
  limit?: number;
20
26
  expanded: boolean;
27
+ xaiSearchMode?: SearchQueryParams["xai_search_mode"];
28
+ allowedDomains?: string[];
29
+ excludedDomains?: string[];
30
+ allowedXHandles?: string[];
31
+ excludedXHandles?: string[];
32
+ fromDate?: string;
33
+ toDate?: string;
34
+ enableImageUnderstanding?: boolean;
35
+ enableImageSearch?: boolean;
36
+ enableVideoUnderstanding?: boolean;
37
+ noInlineCitations?: boolean;
21
38
  }
22
39
 
23
40
  const PROVIDERS: Array<SearchProviderId | "auto"> = ["auto", ...SEARCH_PROVIDER_ORDER];
24
41
 
25
42
  const RECENCY_OPTIONS: SearchCommandArgs["recency"][] = ["day", "week", "month", "year"];
43
+ const XAI_SEARCH_MODES: Array<NonNullable<SearchCommandArgs["xaiSearchMode"]>> = ["web", "x", "web_and_x"];
44
+
45
+ function appendCsv(existing: string[] | undefined, raw: string | undefined): string[] | undefined {
46
+ const values = raw
47
+ ?.split(",")
48
+ .map(value => value.trim())
49
+ .filter(Boolean);
50
+ if (!values?.length) return existing;
51
+ return [...(existing ?? []), ...values];
52
+ }
53
+
54
+ function splitFlag(raw: string): { flag: string; inlineValue?: string } {
55
+ const equals = raw.indexOf("=");
56
+ if (raw.startsWith("-") && equals > 0) return { flag: raw.slice(0, equals), inlineValue: raw.slice(equals + 1) };
57
+ return { flag: raw };
58
+ }
59
+
60
+ function readValue(args: string[], index: number, flag: string, inlineValue: string | undefined): string {
61
+ if (inlineValue !== undefined) return inlineValue;
62
+ const value = args[index + 1];
63
+ if (!value) throw new Error(`${flag} requires a value`);
64
+ return value;
65
+ }
26
66
 
27
67
  /**
28
68
  * Parse web search subcommand arguments.
@@ -41,17 +81,53 @@ export function parseSearchArgs(args: string[]): SearchCommandArgs | undefined {
41
81
  const positional: string[] = [];
42
82
 
43
83
  for (let i = 1; i < args.length; i++) {
44
- const arg = args[i];
84
+ const { flag: arg, inlineValue } = splitFlag(args[i]);
85
+ const value = () => readValue(args, i, arg, inlineValue);
86
+ const consumeSeparateValue = () => {
87
+ if (inlineValue === undefined) i++;
88
+ };
45
89
  if (arg === "--provider") {
46
- result.provider = args[++i] as SearchCommandArgs["provider"];
90
+ result.provider = value() as SearchCommandArgs["provider"];
91
+ consumeSeparateValue();
47
92
  } else if (arg === "--recency") {
48
- result.recency = args[++i] as SearchCommandArgs["recency"];
93
+ result.recency = value() as SearchCommandArgs["recency"];
94
+ consumeSeparateValue();
49
95
  } else if (arg === "--limit" || arg === "-l") {
50
- result.limit = Number.parseInt(args[++i], 10);
96
+ result.limit = Number.parseInt(value(), 10);
97
+ consumeSeparateValue();
98
+ } else if (arg === "--xai-mode") {
99
+ result.xaiSearchMode = value() as SearchCommandArgs["xaiSearchMode"];
100
+ consumeSeparateValue();
101
+ } else if (arg === "--allowed-domain" || arg === "--allowed-domains") {
102
+ result.allowedDomains = appendCsv(result.allowedDomains, value());
103
+ consumeSeparateValue();
104
+ } else if (arg === "--excluded-domain" || arg === "--excluded-domains") {
105
+ result.excludedDomains = appendCsv(result.excludedDomains, value());
106
+ consumeSeparateValue();
107
+ } else if (arg === "--allowed-x-handle" || arg === "--allowed-x-handles") {
108
+ result.allowedXHandles = appendCsv(result.allowedXHandles, value());
109
+ consumeSeparateValue();
110
+ } else if (arg === "--excluded-x-handle" || arg === "--excluded-x-handles") {
111
+ result.excludedXHandles = appendCsv(result.excludedXHandles, value());
112
+ consumeSeparateValue();
113
+ } else if (arg === "--from-date") {
114
+ result.fromDate = value();
115
+ consumeSeparateValue();
116
+ } else if (arg === "--to-date") {
117
+ result.toDate = value();
118
+ consumeSeparateValue();
119
+ } else if (arg === "--image-understanding") {
120
+ result.enableImageUnderstanding = true;
121
+ } else if (arg === "--image-search") {
122
+ result.enableImageSearch = true;
123
+ } else if (arg === "--video-understanding") {
124
+ result.enableVideoUnderstanding = true;
125
+ } else if (arg === "--no-inline-citations") {
126
+ result.noInlineCitations = true;
51
127
  } else if (arg === "--compact") {
52
128
  result.expanded = false;
53
129
  } else if (!arg.startsWith("-")) {
54
- positional.push(arg);
130
+ positional.push(args[i]);
55
131
  }
56
132
  }
57
133
 
@@ -80,18 +156,46 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
80
156
  process.exit(1);
81
157
  }
82
158
 
159
+ if (cmd.xaiSearchMode && !XAI_SEARCH_MODES.includes(cmd.xaiSearchMode)) {
160
+ process.stderr.write(`${chalk.red(`Error: Invalid xAI mode "${cmd.xaiSearchMode}"`)}\n`);
161
+ process.stderr.write(`${chalk.dim(`Valid xAI modes: ${XAI_SEARCH_MODES.join(", ")}`)}\n`);
162
+ process.exit(1);
163
+ }
164
+
83
165
  if (cmd.limit !== undefined && Number.isNaN(cmd.limit)) {
84
166
  process.stderr.write(`${chalk.red("Error: --limit must be a number")}\n`);
85
167
  process.exit(1);
86
168
  }
87
169
 
88
170
  await initTheme();
171
+ const settings = await Settings.init();
172
+ const configuredProvider = settings.get("providers.webSearch");
173
+ if (typeof configuredProvider === "string" && isSearchProviderPreference(configuredProvider)) {
174
+ setPreferredSearchProvider(configuredProvider);
175
+ }
176
+ const configuredFallback = settings.get("web_search.fallback");
177
+ if (Array.isArray(configuredFallback)) {
178
+ setSearchFallbackProviders(
179
+ configuredFallback.filter(value => typeof value === "string" && isConfigurableSearchProviderId(value)),
180
+ );
181
+ }
89
182
 
90
183
  const params: SearchQueryParams = {
91
184
  query: cmd.query,
92
185
  provider: cmd.provider,
93
186
  recency: cmd.recency,
94
187
  limit: cmd.limit,
188
+ xai_search_mode: cmd.xaiSearchMode,
189
+ allowed_domains: cmd.allowedDomains,
190
+ excluded_domains: cmd.excludedDomains,
191
+ allowed_x_handles: cmd.allowedXHandles,
192
+ excluded_x_handles: cmd.excludedXHandles,
193
+ from_date: cmd.fromDate,
194
+ to_date: cmd.toDate,
195
+ enable_image_understanding: cmd.enableImageUnderstanding,
196
+ enable_image_search: cmd.enableImageSearch,
197
+ enable_video_understanding: cmd.enableVideoUnderstanding,
198
+ no_inline_citations: cmd.noInlineCitations,
95
199
  };
96
200
 
97
201
  const result = await runSearchQuery(params);
@@ -121,7 +225,18 @@ ${chalk.bold("Arguments:")}
121
225
 
122
226
  ${chalk.bold("Options:")}
123
227
  --provider <name> Provider: ${PROVIDERS.join(", ")}
124
- --recency <value> Recency filter (Brave/Perplexity): ${RECENCY_OPTIONS.join(", ")}
228
+ --recency <value> Recency filter: ${RECENCY_OPTIONS.join(", ")}
229
+ --xai-mode <mode> xAI mode: web, x, web_and_x
230
+ --allowed-domain(s) d xAI web_search domain allow-list (comma-separated, repeatable)
231
+ --excluded-domain(s) d xAI web_search domain deny-list (comma-separated, repeatable)
232
+ --allowed-x-handle(s) h xAI x_search handle allow-list (comma-separated, repeatable)
233
+ --excluded-x-handle(s) h xAI x_search handle deny-list (comma-separated, repeatable)
234
+ --from-date <date> xAI x_search start date (ISO8601)
235
+ --to-date <date> xAI x_search end date (ISO8601)
236
+ --image-understanding Enable xAI image understanding
237
+ --image-search Enable xAI web image search
238
+ --video-understanding Enable xAI X video understanding
239
+ --no-inline-citations Disable xAI inline citation markdown
125
240
  -l, --limit <n> Max results to return
126
241
  --compact Render condensed output
127
242
  -h, --help Show this help
package/src/cli.ts CHANGED
@@ -35,6 +35,7 @@ const commands: CommandEntry[] = [
35
35
  { name: "gc", load: () => import("./commands/gc").then(m => m.default) },
36
36
  { name: "ralplan", load: () => import("./commands/ralplan").then(m => m.default) },
37
37
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
38
+ { name: "web-search", aliases: ["q"], load: () => import("./commands/web-search").then(m => m.default) },
38
39
  { name: "mcp-serve", load: () => import("./commands/mcp-serve").then(m => m.default) },
39
40
  {
40
41
  name: "contribute-pr",
@@ -42,6 +43,7 @@ const commands: CommandEntry[] = [
42
43
  load: () => import("./commands/contribution-prep").then(m => m.default),
43
44
  },
44
45
  { name: "deep-interview", load: () => import("./commands/deep-interview").then(m => m.default) },
46
+ { name: "rlm", load: () => import("./commands/rlm").then(m => m.default) },
45
47
  { name: "update", load: () => import("./commands/update").then(m => m.default) },
46
48
  { name: "launch", load: () => import("./commands/launch").then(m => m.default) },
47
49
  ];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `gjc rlm` — opt-in Jupyter-style research session with a persistent Python kernel.
3
+ */
4
+ import { Command } from "@gajae-code/utils/cli";
5
+ import { runRlmCommand } from "../rlm";
6
+
7
+ export default class Rlm extends Command {
8
+ static description = "Opt-in research session: persistent Python kernel, live notebook, synthesized report";
9
+ static strict = false;
10
+ static examples = [
11
+ "# Start an interactive research session\n gjc rlm",
12
+ '# Seed the session with an initial question\n gjc rlm "What drives the spike in the orders table?"',
13
+ "# Point the session at a data description\n gjc rlm --data ./datasets/DATA.md",
14
+ ];
15
+
16
+ async run(): Promise<void> {
17
+ await runRlmCommand(this.argv);
18
+ }
19
+ }
@@ -9,6 +9,22 @@ const PROVIDERS: Array<string> = ["auto", ...SEARCH_PROVIDER_ORDER];
9
9
 
10
10
  const RECENCY: NonNullable<SearchCommandArgs["recency"]>[] = ["day", "week", "month", "year"];
11
11
 
12
+ type ListFlagValue = string | string[] | undefined;
13
+
14
+ function appendCsv(existing: string[] | undefined, raw: ListFlagValue): string[] | undefined {
15
+ const rawValues = Array.isArray(raw) ? raw : raw === undefined ? [] : [raw];
16
+ const values = rawValues
17
+ .flatMap(value => value.split(","))
18
+ .map(value => value.trim())
19
+ .filter(Boolean);
20
+ if (values.length === 0) return existing;
21
+ return [...(existing ?? []), ...values];
22
+ }
23
+
24
+ function combineCsv(...values: ListFlagValue[]): string[] | undefined {
25
+ return values.reduce<string[] | undefined>((acc, value) => appendCsv(acc, value), undefined);
26
+ }
27
+
12
28
  export default class Search extends Command {
13
29
  static description = "Test web search providers";
14
30
 
@@ -22,6 +38,45 @@ export default class Search extends Command {
22
38
  provider: Flags.string({ description: "Search provider", options: PROVIDERS }),
23
39
  recency: Flags.string({ description: "Recency filter", options: RECENCY }),
24
40
  limit: Flags.integer({ char: "l", description: "Max results to return" }),
41
+ "xai-mode": Flags.string({ description: "xAI mode", options: ["web", "x", "web_and_x"] }),
42
+ "allowed-domain": Flags.string({
43
+ description: "xAI web_search allowed domains, comma-separated",
44
+ multiple: true,
45
+ }),
46
+ "allowed-domains": Flags.string({
47
+ description: "xAI web_search allowed domains, comma-separated",
48
+ multiple: true,
49
+ }),
50
+ "excluded-domain": Flags.string({
51
+ description: "xAI web_search excluded domains, comma-separated",
52
+ multiple: true,
53
+ }),
54
+ "excluded-domains": Flags.string({
55
+ description: "xAI web_search excluded domains, comma-separated",
56
+ multiple: true,
57
+ }),
58
+ "allowed-x-handle": Flags.string({
59
+ description: "xAI x_search allowed handles, comma-separated",
60
+ multiple: true,
61
+ }),
62
+ "allowed-x-handles": Flags.string({
63
+ description: "xAI x_search allowed handles, comma-separated",
64
+ multiple: true,
65
+ }),
66
+ "excluded-x-handle": Flags.string({
67
+ description: "xAI x_search excluded handles, comma-separated",
68
+ multiple: true,
69
+ }),
70
+ "excluded-x-handles": Flags.string({
71
+ description: "xAI x_search excluded handles, comma-separated",
72
+ multiple: true,
73
+ }),
74
+ "from-date": Flags.string({ description: "xAI x_search start date (ISO8601)" }),
75
+ "to-date": Flags.string({ description: "xAI x_search end date (ISO8601)" }),
76
+ "image-understanding": Flags.boolean({ description: "Enable xAI image understanding" }),
77
+ "image-search": Flags.boolean({ description: "Enable xAI web image search" }),
78
+ "video-understanding": Flags.boolean({ description: "Enable xAI X video understanding" }),
79
+ "no-inline-citations": Flags.boolean({ description: "Disable xAI inline citation markdown" }),
25
80
  compact: Flags.boolean({ description: "Render condensed output" }),
26
81
  };
27
82
 
@@ -35,6 +90,17 @@ export default class Search extends Command {
35
90
  recency: flags.recency as SearchCommandArgs["recency"],
36
91
  limit: flags.limit,
37
92
  expanded: !flags.compact,
93
+ xaiSearchMode: flags["xai-mode"] as SearchCommandArgs["xaiSearchMode"],
94
+ allowedDomains: combineCsv(flags["allowed-domain"], flags["allowed-domains"]),
95
+ excludedDomains: combineCsv(flags["excluded-domain"], flags["excluded-domains"]),
96
+ allowedXHandles: combineCsv(flags["allowed-x-handle"], flags["allowed-x-handles"]),
97
+ excludedXHandles: combineCsv(flags["excluded-x-handle"], flags["excluded-x-handles"]),
98
+ fromDate: flags["from-date"],
99
+ toDate: flags["to-date"],
100
+ enableImageUnderstanding: flags["image-understanding"],
101
+ enableImageSearch: flags["image-search"],
102
+ enableVideoUnderstanding: flags["video-understanding"],
103
+ noInlineCitations: flags["no-inline-citations"],
38
104
  };
39
105
 
40
106
  await runSearchCommand(cmd);
@@ -27,8 +27,10 @@ interface AppKeybindings {
27
27
  "app.model.select": true;
28
28
  "app.model.selectTemporary": true;
29
29
  "app.tools.expand": true;
30
+ "app.tool.backgroundFold": true;
30
31
  "app.editor.external": true;
31
32
  "app.message.followUp": true;
33
+ "app.message.queue": true;
32
34
  "app.message.dequeue": true;
33
35
  "app.clipboard.pasteImage": true;
34
36
  "app.clipboard.copyLine": true;
@@ -106,6 +108,10 @@ export const KEYBINDINGS = {
106
108
  defaultKeys: "ctrl+o",
107
109
  description: "Expand tools",
108
110
  },
111
+ "app.tool.backgroundFold": {
112
+ defaultKeys: "ctrl+b",
113
+ description: "Fold/background supported foreground tool",
114
+ },
109
115
  "app.editor.external": {
110
116
  defaultKeys: "ctrl+g",
111
117
  description: "Open external editor",
@@ -114,6 +120,10 @@ export const KEYBINDINGS = {
114
120
  defaultKeys: "ctrl+enter",
115
121
  description: "Send follow-up message",
116
122
  },
123
+ "app.message.queue": {
124
+ defaultKeys: "alt+enter",
125
+ description: "Queue message for next turn",
126
+ },
117
127
  "app.message.dequeue": {
118
128
  defaultKeys: "alt+up",
119
129
  description: "Dequeue message",
@@ -217,6 +227,7 @@ const KEYBINDING_NAME_MIGRATIONS = {
217
227
  toggleThinking: "app.thinking.toggle",
218
228
  externalEditor: "app.editor.external",
219
229
  followUp: "app.message.followUp",
230
+ queue: "app.message.queue",
220
231
  dequeue: "app.message.dequeue",
221
232
  pasteImage: "app.clipboard.pasteImage",
222
233
  copyLine: "app.clipboard.copyLine",
@@ -6,6 +6,7 @@ export type ModelProfileRole = GjcModelAssignmentTargetId;
6
6
  export interface ModelProfileDefinition {
7
7
  name: string;
8
8
  requiredProviders: string[];
9
+ displayName?: string;
9
10
  /**
10
11
  * Optional groups of providers that are interchangeable fallbacks.
11
12
  * Each group is an array of provider ids where at least one must be
@@ -323,8 +324,14 @@ const PROFILE_RECOMMENDATIONS: Record<string, string> = {
323
324
  "minimax-code": "minimax-medium",
324
325
  };
325
326
 
326
- export function getModelProfilePresentation(name: string): ModelProfilePresentation {
327
- return PROFILE_PRESENTATION[name] ?? { displayName: name, providerGroup: "COMBOS" };
327
+ export function getModelProfilePresentation(
328
+ profile: string | Pick<ModelProfileDefinition, "name" | "displayName">,
329
+ ): ModelProfilePresentation {
330
+ const name = typeof profile === "string" ? profile : profile.name;
331
+ const displayName = typeof profile === "string" ? undefined : profile.displayName;
332
+ const presentation = PROFILE_PRESENTATION[name];
333
+ if (presentation) return presentation;
334
+ return { displayName: displayName ?? name, providerGroup: "CUSTOM" };
328
335
  }
329
336
 
330
337
  export function groupModelProfilesForPresetLanding(
@@ -333,7 +340,7 @@ export function groupModelProfilesForPresetLanding(
333
340
  const groups = new Map<string, ModelProfileDefinition[]>();
334
341
  for (const group of PROFILE_GROUP_ORDER) groups.set(group, []);
335
342
  for (const profile of profiles.values()) {
336
- const group = getModelProfilePresentation(profile.name).providerGroup;
343
+ const group = getModelProfilePresentation(profile).providerGroup;
337
344
  if (!groups.has(group)) groups.set(group, []);
338
345
  groups.get(group)?.push(profile);
339
346
  }
@@ -365,6 +372,7 @@ export function mergeModelProfiles(userProfiles?: ModelsConfig["profiles"]): Map
365
372
  const modelMapping = { ...definition.model_mapping };
366
373
  profiles.set(name, {
367
374
  name,
375
+ displayName: definition.display_name,
368
376
  requiredProviders: aggregateModelProfileRequiredProviders(definition.required_providers, { modelMapping }),
369
377
  modelMapping,
370
378
  source: "user",
@@ -1,3 +1,4 @@
1
+ import * as fs from "node:fs/promises";
1
2
  import * as path from "node:path";
2
3
  import {
3
4
  type Api,
@@ -45,11 +46,17 @@ import {
45
46
  formatCanonicalVariantSelector,
46
47
  type ModelEquivalenceConfig,
47
48
  } from "./model-equivalence";
48
- import { type ModelProfileDefinition, mergeModelProfiles } from "./model-profiles";
49
+ import {
50
+ aggregateModelProfileRequiredProviders,
51
+ type ModelProfileDefinition,
52
+ mergeModelProfiles,
53
+ } from "./model-profiles";
49
54
  import {
50
55
  type ModelOverride,
56
+ type ModelProfileConfig,
51
57
  type ModelsConfig,
52
58
  ModelsConfigSchema,
59
+ ProfileDefinitionSchema,
53
60
  type ProviderAuthMode,
54
61
  type ProviderDiscovery,
55
62
  } from "./models-config-schema";
@@ -1489,6 +1496,53 @@ export class ModelRegistry {
1489
1496
  getAvailableModelProfileNames(): string[] {
1490
1497
  return [...this.#modelProfiles.keys()].sort((a, b) => a.localeCompare(b));
1491
1498
  }
1499
+
1500
+ async saveCustomModelProfile(name: string, definition: ModelProfileConfig): Promise<ModelProfileDefinition> {
1501
+ const normalizedName = name.trim();
1502
+ if (!normalizedName) throw new Error("Profile name is required.");
1503
+ const checkedDefinition = ProfileDefinitionSchema.safeParse(definition);
1504
+ if (!checkedDefinition.success) {
1505
+ const first = checkedDefinition.error.issues[0];
1506
+ const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
1507
+ throw new Error(`Custom model profile is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
1508
+ }
1509
+ const loaded = this.#modelsConfigFile.tryLoad();
1510
+ if (loaded.status === "error") {
1511
+ throw new Error(
1512
+ `Cannot create custom model profile because ${this.#modelsConfigFile.path()} is invalid. Fix the existing config before saving a new preset.`,
1513
+ );
1514
+ }
1515
+ const current = loaded.value ?? this.#modelsConfigFile.createDefault();
1516
+ if (mergeModelProfiles(current.profiles).has(normalizedName)) {
1517
+ throw new Error(`Custom model profile already exists: ${normalizedName}. Choose a unique preset id.`);
1518
+ }
1519
+ const next: ModelsConfig = {
1520
+ ...current,
1521
+ profiles: {
1522
+ ...(current.profiles ?? {}),
1523
+ [normalizedName]: definition,
1524
+ },
1525
+ };
1526
+ const checkedConfig = ModelsConfigSchema.safeParse(next);
1527
+ if (!checkedConfig.success) {
1528
+ const first = checkedConfig.error.issues[0];
1529
+ const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
1530
+ throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
1531
+ }
1532
+ await fs.mkdir(path.dirname(this.#modelsConfigFile.path()), { recursive: true });
1533
+ await Bun.write(this.#modelsConfigFile.path(), Bun.YAML.stringify(checkedConfig.data, null, 2));
1534
+ this.#modelsConfigFile.invalidate();
1535
+ this.#reloadStaticModels();
1536
+ const modelMapping = { ...definition.model_mapping };
1537
+ const profile: ModelProfileDefinition = {
1538
+ name: normalizedName,
1539
+ displayName: definition.display_name,
1540
+ requiredProviders: aggregateModelProfileRequiredProviders(definition.required_providers, { modelMapping }),
1541
+ modelMapping,
1542
+ source: "user",
1543
+ };
1544
+ return profile;
1545
+ }
1492
1546
  applyConfiguredModelBindings(targetSettings: Settings): void {
1493
1547
  this.#modelBindingsTargetSettings = targetSettings;
1494
1548
  this.#applyConfiguredModelBindingsToTarget();
@@ -108,6 +108,7 @@ export const ProfileModelMappingSchema = z.partialRecord(ProfileRoleSchema, Prof
108
108
  export const ProfileDefinitionSchema = z
109
109
  .object({
110
110
  required_providers: z.array(z.string().min(1)).min(1),
111
+ display_name: z.string().min(1).optional(),
111
112
  model_mapping: ProfileModelMappingSchema,
112
113
  })
113
114
  .strict();