@gajae-code/coding-agent 0.7.1 → 0.7.3

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 (135) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli/notify-cli.d.ts +2 -0
  4. package/dist/types/cli.d.ts +6 -0
  5. package/dist/types/commands/mcp.d.ts +70 -0
  6. package/dist/types/config/keybindings.d.ts +2 -2
  7. package/dist/types/config/settings-schema.d.ts +39 -2
  8. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  9. package/dist/types/extensibility/shared-events.d.ts +1 -0
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +1 -1
  11. package/dist/types/lsp/types.d.ts +2 -0
  12. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  13. package/dist/types/modes/components/model-selector.d.ts +2 -0
  14. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  15. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  16. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  17. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  18. package/dist/types/notifications/config.d.ts +9 -1
  19. package/dist/types/notifications/engine.d.ts +59 -0
  20. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  21. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  22. package/dist/types/notifications/telegram-daemon.d.ts +73 -16
  23. package/dist/types/notifications/threaded-inbound.d.ts +19 -0
  24. package/dist/types/notifications/threaded-render.d.ts +6 -1
  25. package/dist/types/notifications/topic-registry.d.ts +2 -0
  26. package/dist/types/session/agent-session.d.ts +2 -0
  27. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  28. package/dist/types/tools/fetch.d.ts +23 -0
  29. package/dist/types/tools/index.d.ts +1 -0
  30. package/dist/types/tools/telegram-send.d.ts +32 -0
  31. package/dist/types/web/insane/bridge.d.ts +103 -0
  32. package/dist/types/web/insane/url-guard.d.ts +25 -0
  33. package/dist/types/web/scrapers/types.d.ts +5 -0
  34. package/dist/types/web/scrapers/utils.d.ts +7 -1
  35. package/dist/types/web/search/provider.d.ts +18 -1
  36. package/dist/types/web/search/providers/insane.d.ts +53 -0
  37. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  38. package/dist/types/web/search/types.d.ts +12 -4
  39. package/package.json +10 -8
  40. package/scripts/verify-insane-vendor.ts +132 -0
  41. package/src/cli/args.ts +1 -1
  42. package/src/cli/fast-help.ts +1 -1
  43. package/src/cli/mcp-cli.ts +272 -0
  44. package/src/cli/notify-cli.ts +152 -5
  45. package/src/cli.ts +6 -2
  46. package/src/commands/mcp.ts +117 -0
  47. package/src/commands/team.ts +1 -1
  48. package/src/config/keybindings.ts +2 -2
  49. package/src/config/settings-schema.ts +30 -1
  50. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  51. package/src/defaults/gjc/skills/deep-interview/SKILL.md +4 -3
  52. package/src/defaults/gjc/skills/ralplan/SKILL.md +11 -4
  53. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  54. package/src/extensibility/extensions/runner.ts +1 -0
  55. package/src/extensibility/shared-events.ts +1 -0
  56. package/src/gjc-runtime/launch-tmux.ts +17 -3
  57. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  58. package/src/gjc-runtime/ralplan-runtime.ts +2 -2
  59. package/src/gjc-runtime/tmux-common.ts +3 -1
  60. package/src/gjc-runtime/ultragoal-guard.ts +25 -8
  61. package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
  62. package/src/gjc-runtime/workflow-manifest.ts +7 -2
  63. package/src/hooks/skill-state.ts +57 -0
  64. package/src/internal-urls/docs-index.generated.ts +14 -11
  65. package/src/lsp/config.ts +16 -3
  66. package/src/lsp/defaults.json +7 -0
  67. package/src/lsp/types.ts +2 -0
  68. package/src/modes/bridge/bridge-mode.ts +11 -0
  69. package/src/modes/components/custom-editor.ts +2 -0
  70. package/src/modes/components/footer.ts +2 -3
  71. package/src/modes/components/model-selector.ts +12 -0
  72. package/src/modes/components/status-line/git-utils.ts +25 -0
  73. package/src/modes/components/status-line.ts +10 -11
  74. package/src/modes/components/welcome.ts +2 -3
  75. package/src/modes/controllers/event-controller.ts +15 -0
  76. package/src/modes/controllers/selector-controller.ts +3 -0
  77. package/src/modes/interactive-mode.ts +48 -3
  78. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  79. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  80. package/src/modes/theme/defaults/index.ts +2 -0
  81. package/src/modes/utils/context-usage.ts +2 -2
  82. package/src/notifications/attachment-registry.ts +23 -0
  83. package/src/notifications/chat-adapters.ts +147 -0
  84. package/src/notifications/config.ts +23 -2
  85. package/src/notifications/engine.ts +100 -0
  86. package/src/notifications/index.ts +180 -38
  87. package/src/notifications/managed-daemon.ts +163 -0
  88. package/src/notifications/operator-runtime.ts +171 -0
  89. package/src/notifications/telegram-daemon.ts +553 -236
  90. package/src/notifications/threaded-inbound.ts +60 -4
  91. package/src/notifications/threaded-render.ts +20 -2
  92. package/src/notifications/topic-registry.ts +5 -0
  93. package/src/session/agent-session.ts +82 -51
  94. package/src/slash-commands/helpers/parse.ts +2 -1
  95. package/src/tools/bash.ts +9 -0
  96. package/src/tools/composer-bash-policy.ts +96 -0
  97. package/src/tools/fetch.ts +94 -1
  98. package/src/tools/index.ts +3 -0
  99. package/src/tools/telegram-send.ts +137 -0
  100. package/src/web/insane/bridge.ts +350 -0
  101. package/src/web/insane/url-guard.ts +159 -0
  102. package/src/web/scrapers/types.ts +143 -45
  103. package/src/web/scrapers/utils.ts +70 -19
  104. package/src/web/search/provider.ts +77 -18
  105. package/src/web/search/providers/anthropic.ts +70 -3
  106. package/src/web/search/providers/codex.ts +1 -119
  107. package/src/web/search/providers/gemini.ts +99 -0
  108. package/src/web/search/providers/insane.ts +551 -0
  109. package/src/web/search/providers/openai-compatible.ts +66 -32
  110. package/src/web/search/providers/text-citations.ts +111 -0
  111. package/src/web/search/types.ts +13 -2
  112. package/vendor/insane-search/LICENSE +21 -0
  113. package/vendor/insane-search/MANIFEST.json +24 -0
  114. package/vendor/insane-search/engine/__init__.py +23 -0
  115. package/vendor/insane-search/engine/__main__.py +128 -0
  116. package/vendor/insane-search/engine/bias_check.py +183 -0
  117. package/vendor/insane-search/engine/executor.py +254 -0
  118. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  119. package/vendor/insane-search/engine/learning.py +175 -0
  120. package/vendor/insane-search/engine/phase0.py +214 -0
  121. package/vendor/insane-search/engine/safety.py +91 -0
  122. package/vendor/insane-search/engine/templates/package.json +11 -0
  123. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  124. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  125. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  126. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  127. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  128. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  129. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  130. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  131. package/vendor/insane-search/engine/transport.py +211 -0
  132. package/vendor/insane-search/engine/url_transforms.py +98 -0
  133. package/vendor/insane-search/engine/validators.py +331 -0
  134. package/vendor/insane-search/engine/waf_detector.py +214 -0
  135. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
@@ -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" | "xai" | "tavily" | "parallel" | "kagi" | "synthetic" | "searxng" | "openai-compatible";
7
+ export type SearchProviderId = "duckduckgo" | "insane" | "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", "xai", "tavily", "parallel", "kagi", "synthetic", "searxng"];
18
+ export declare const CONFIGURABLE_SEARCH_PROVIDER_IDS: readonly ["duckduckgo", "insane", "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";
@@ -96,6 +96,14 @@ export interface AnthropicCitation {
96
96
  cited_text: string;
97
97
  encrypted_index: string;
98
98
  }
99
+ /**
100
+ * Error payload returned in `web_search_tool_result.content` when a server-side
101
+ * web search fails. Unlike the success case, this is an object, not an array.
102
+ */
103
+ export interface AnthropicWebSearchToolResultError {
104
+ type: "web_search_tool_result_error";
105
+ error_code?: string;
106
+ }
99
107
  export interface AnthropicContentBlock {
100
108
  type: string;
101
109
  /** Text content (for type="text") */
@@ -108,8 +116,8 @@ export interface AnthropicContentBlock {
108
116
  input?: {
109
117
  query: string;
110
118
  };
111
- /** Search results (for type="web_search_tool_result") */
112
- content?: AnthropicSearchResult[];
119
+ /** Search results array on success, or an error object on failure (type="web_search_tool_result") */
120
+ content?: AnthropicSearchResult[] | AnthropicWebSearchToolResultError;
113
121
  }
114
122
  export interface AnthropicApiResponse {
115
123
  id: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.7.1",
4
+ "version": "0.7.3",
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",
@@ -45,18 +45,19 @@
45
45
  "generate-docs-index": "bun scripts/generate-docs-index.ts",
46
46
  "prepack": "bun scripts/generate-docs-index.ts",
47
47
  "generate-template": "bun scripts/generate-template.ts",
48
- "install:defaults": "bun src/cli.ts setup defaults"
48
+ "install:defaults": "bun src/cli.ts setup defaults",
49
+ "verify:insane-vendor": "bun scripts/verify-insane-vendor.ts"
49
50
  },
50
51
  "dependencies": {
51
52
  "@agentclientprotocol/sdk": "0.21.0",
52
53
  "@babel/parser": "^7.29.3",
53
54
  "@mozilla/readability": "^0.6.0",
54
- "@gajae-code/stats": "0.7.1",
55
- "@gajae-code/agent-core": "0.7.1",
56
- "@gajae-code/ai": "0.7.1",
57
- "@gajae-code/natives": "0.7.1",
58
- "@gajae-code/tui": "0.7.1",
59
- "@gajae-code/utils": "0.7.1",
55
+ "@gajae-code/stats": "0.7.3",
56
+ "@gajae-code/agent-core": "0.7.3",
57
+ "@gajae-code/ai": "0.7.3",
58
+ "@gajae-code/natives": "0.7.3",
59
+ "@gajae-code/tui": "0.7.3",
60
+ "@gajae-code/utils": "0.7.3",
60
61
  "@puppeteer/browsers": "^2.13.0",
61
62
  "@types/turndown": "5.0.6",
62
63
  "@xterm/headless": "^6.0.0",
@@ -84,6 +85,7 @@
84
85
  "examples",
85
86
  "README.md",
86
87
  "CHANGELOG.md",
88
+ "vendor",
87
89
  "dist/types"
88
90
  ],
89
91
  "exports": {
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Vendor verification for packages/coding-agent/vendor/insane-search.
4
+ *
5
+ * Asserts:
6
+ * 1. Forbidden upstream paths/patterns are absent (install hooks, star-baiting,
7
+ * update-notifier, transcript-language scanner, .claude-plugin).
8
+ * 2. MANIFEST.json exists and pins a full 40-char upstream commit SHA.
9
+ * 3. The vendored runtime files are present.
10
+ * 4. `npm pack --dry-run --json` includes the vendor tree (engine entrypoint,
11
+ * templates, LICENSE, manifest) in the published package.
12
+ *
13
+ * Exit code 0 on success, 1 on any failure.
14
+ */
15
+ import { execFileSync } from "node:child_process";
16
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
17
+ import { dirname, join, relative } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const pkgDir = join(dirname(fileURLToPath(import.meta.url)), "..");
21
+ const vendorDir = join(pkgDir, "vendor", "insane-search");
22
+ const failures: string[] = [];
23
+
24
+ function fail(msg: string): void {
25
+ failures.push(msg);
26
+ }
27
+
28
+ function walk(dir: string): string[] {
29
+ const out: string[] = [];
30
+ for (const entry of readdirSync(dir)) {
31
+ const full = join(dir, entry);
32
+ if (statSync(full).isDirectory()) out.push(...walk(full));
33
+ else out.push(full);
34
+ }
35
+ return out;
36
+ }
37
+
38
+ // 1. Forbidden paths / patterns
39
+ if (!existsSync(vendorDir)) {
40
+ fail(`vendor tree missing at ${vendorDir}`);
41
+ } else {
42
+ const files = walk(vendorDir).map(f => relative(vendorDir, f));
43
+ const forbiddenNames = ["setup.sh", "gptaku-update-check.cjs"];
44
+ const forbiddenSubpaths = [".claude-plugin/", "/references/", "/tests/coverage_battery"];
45
+ for (const f of files) {
46
+ const base = f.split("/").pop() ?? f;
47
+ if (forbiddenNames.includes(base)) fail(`forbidden file present: ${f}`);
48
+ for (const sub of forbiddenSubpaths) {
49
+ if (`/${f}`.includes(sub)) fail(`forbidden path present: ${f}`);
50
+ }
51
+ }
52
+ // Scan for star-baiting / settings.json mutation / transcript scanning patterns.
53
+ const forbiddenPatterns: Array<[RegExp, string]> = [
54
+ [/user\/starred/, "github star-baiting (user/starred)"],
55
+ [/gh\s+api\s+-X\s+PUT/, "gh api star write"],
56
+ [/SessionStart/, "settings.json SessionStart hook injection"],
57
+ [/\.claude\/projects/, "past-session transcript scanner"],
58
+ ];
59
+ for (const f of files) {
60
+ if (f === "MANIFEST.json") continue; // manifest documents the excluded patterns by name
61
+ let body = "";
62
+ try {
63
+ body = readFileSync(join(vendorDir, f), "utf8");
64
+ } catch {
65
+ continue;
66
+ }
67
+ for (const [re, label] of forbiddenPatterns) {
68
+ if (re.test(body)) fail(`forbidden pattern (${label}) found in ${f}`);
69
+ }
70
+ }
71
+ }
72
+
73
+ // 2. Manifest with pinned SHA
74
+ const manifestPath = join(vendorDir, "MANIFEST.json");
75
+ let commit = "";
76
+ if (!existsSync(manifestPath)) {
77
+ fail("MANIFEST.json missing");
78
+ } else {
79
+ try {
80
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as {
81
+ upstream?: { commit?: string };
82
+ };
83
+ commit = manifest.upstream?.commit ?? "";
84
+ if (!/^[0-9a-f]{40}$/.test(commit)) fail(`MANIFEST upstream.commit is not a full 40-char SHA: "${commit}"`);
85
+ } catch (err) {
86
+ fail(`MANIFEST.json is not valid JSON: ${(err as Error).message}`);
87
+ }
88
+ }
89
+
90
+ // 3. Required runtime files present
91
+ const requiredFiles = [
92
+ "engine/__main__.py",
93
+ "engine/__init__.py",
94
+ "engine/fetch_chain.py",
95
+ "engine/templates/package.json",
96
+ "engine/templates/playwright_real_chrome.js",
97
+ "engine/waf_profiles.yaml",
98
+ "LICENSE",
99
+ "MANIFEST.json",
100
+ ];
101
+ for (const rel of requiredFiles) {
102
+ if (!existsSync(join(vendorDir, rel))) fail(`required vendored file missing: ${rel}`);
103
+ }
104
+
105
+ // 4. Package pack inclusion
106
+ try {
107
+ const raw = execFileSync("npm", ["pack", "--dry-run", "--json"], {
108
+ cwd: pkgDir,
109
+ encoding: "utf8",
110
+ stdio: ["ignore", "pipe", "ignore"],
111
+ });
112
+ const parsed = JSON.parse(raw) as Array<{ files?: Array<{ path: string }> }>;
113
+ const packed = new Set((parsed[0]?.files ?? []).map(f => f.path.replace(/\\/g, "/")));
114
+ const mustPack = [
115
+ "vendor/insane-search/engine/__main__.py",
116
+ "vendor/insane-search/engine/templates/playwright_real_chrome.js",
117
+ "vendor/insane-search/LICENSE",
118
+ "vendor/insane-search/MANIFEST.json",
119
+ ];
120
+ for (const rel of mustPack) {
121
+ if (!packed.has(rel)) fail(`package pack does not include ${rel}`);
122
+ }
123
+ } catch (err) {
124
+ fail(`npm pack --dry-run failed: ${(err as Error).message}`);
125
+ }
126
+
127
+ if (failures.length > 0) {
128
+ console.error("insane-vendor verification FAILED:");
129
+ for (const f of failures) console.error(` - ${f}`);
130
+ process.exit(1);
131
+ }
132
+ console.log(`insane-vendor verification passed (pinned ${commit}).`);
package/src/cli/args.ts CHANGED
@@ -265,7 +265,7 @@ export function getExtraHelpText(): string {
265
265
  GJC_SLOW_MODEL - Override slow/reasoning model (see --slow)
266
266
  GJC_PLAN_MODEL - Override planning model (see --plan)
267
267
  GJC_NO_PTY - Disable PTY-based interactive bash execution
268
- --tmux - Launch interactive startup inside a new tmux session
268
+ --tmux - Launch interactive startup inside a fresh tmux session
269
269
  gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions
270
270
  GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
271
271
  GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
@@ -50,7 +50,7 @@ export function getExtraHelpText(): string {
50
50
  GJC_SLOW_MODEL - Override slow/reasoning model (see --slow)
51
51
  GJC_PLAN_MODEL - Override planning model (see --plan)
52
52
  GJC_NO_PTY - Disable PTY-based interactive bash execution
53
- --tmux - Launch interactive startup inside a new tmux session
53
+ --tmux - Launch interactive startup inside a fresh tmux session
54
54
  gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions
55
55
  GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
56
56
  GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Direct MCP server registration CLI helpers.
3
+ *
4
+ * This surface only writes explicit user-provided server definitions to GJC's
5
+ * own MCP config. It never imports or inherits live configs from other agents.
6
+ */
7
+ import { getMCPConfigPath, getProjectDir } from "@gajae-code/utils";
8
+ import { getMCPServer, readMCPConfigFile, removeMCPServer, upsertMCPServer } from "../runtime-mcp/config-writer";
9
+ import type { MCPConfigFile, MCPServerConfig } from "../runtime-mcp/types";
10
+
11
+ export type MCPAction = "add" | "list" | "remove";
12
+
13
+ export interface MCPCommandArgs {
14
+ action: MCPAction;
15
+ name?: string;
16
+ commandArgs?: string[];
17
+ flags: {
18
+ project?: boolean;
19
+ force?: boolean;
20
+ json?: boolean;
21
+ type?: "stdio" | "http" | "sse";
22
+ command?: string;
23
+ url?: string;
24
+ arg?: string[];
25
+ env?: string[];
26
+ header?: string[];
27
+ cwd?: string;
28
+ timeout?: number;
29
+ };
30
+ cwd?: string;
31
+ }
32
+
33
+ export class MCPArgsError extends Error {}
34
+
35
+ interface ScopedPath {
36
+ scope: "user" | "project";
37
+ path: string;
38
+ }
39
+
40
+ interface RedactedServerEntry {
41
+ name: string;
42
+ config: MCPServerConfig;
43
+ }
44
+
45
+ const REDACTED = "<redacted>";
46
+ const SENSITIVE_KEY_PATTERN =
47
+ /(?:token|secret|key|credential|password|passwd|pwd|authorization|auth|bearer|cookie|session)/i;
48
+
49
+ function resolvePath(args: MCPCommandArgs): ScopedPath {
50
+ const scope = args.flags.project ? "project" : "user";
51
+ return { scope, path: getMCPConfigPath(scope, args.cwd ?? getProjectDir()) };
52
+ }
53
+
54
+ function parsePairs(values: string[] | undefined, label: string): Record<string, string> | undefined {
55
+ if (!values || values.length === 0) return undefined;
56
+ const parsed: Record<string, string> = {};
57
+ for (const value of values) {
58
+ const index = value.indexOf("=");
59
+ if (index <= 0) {
60
+ throw new MCPArgsError(`Invalid ${label}. Use KEY=VALUE.`);
61
+ }
62
+ const key = value.slice(0, index).trim();
63
+ if (!key) {
64
+ throw new MCPArgsError(`Invalid ${label}. Key cannot be empty.`);
65
+ }
66
+ parsed[key] = value.slice(index + 1);
67
+ }
68
+ return parsed;
69
+ }
70
+
71
+ function buildServerConfig(args: MCPCommandArgs): MCPServerConfig {
72
+ const type = args.flags.type ?? (args.flags.url ? "http" : "stdio");
73
+ const timeout = args.flags.timeout;
74
+ const shared = timeout === undefined ? {} : { timeout };
75
+
76
+ if (type === "stdio") {
77
+ const command = args.flags.command ?? args.commandArgs?.[0];
78
+ if (!command) {
79
+ throw new MCPArgsError("`gjc mcp add` requires --command <cmd> or a positional command for stdio servers.");
80
+ }
81
+ const config: MCPServerConfig = {
82
+ ...shared,
83
+ type: "stdio",
84
+ command,
85
+ };
86
+ const positionalArgs = args.flags.command ? [] : (args.commandArgs ?? []).slice(1);
87
+ const serverArgs = [...positionalArgs, ...(args.flags.arg ?? [])];
88
+ if (serverArgs.length > 0) config.args = serverArgs;
89
+ const env = parsePairs(args.flags.env, "env");
90
+ if (env) config.env = env;
91
+ if (args.flags.cwd) config.cwd = args.flags.cwd;
92
+ return config;
93
+ }
94
+
95
+ const url = args.flags.url ?? args.commandArgs?.[0];
96
+ if (!url) {
97
+ throw new MCPArgsError(`\`gjc mcp add --type ${type}\` requires --url <url> or a positional URL.`);
98
+ }
99
+ const headers = parsePairs(args.flags.header, "header");
100
+ if (type === "http") {
101
+ const config: MCPServerConfig = {
102
+ ...shared,
103
+ type,
104
+ url,
105
+ };
106
+ if (headers) config.headers = headers;
107
+ return config;
108
+ }
109
+ const config: MCPServerConfig = {
110
+ ...shared,
111
+ type,
112
+ url,
113
+ };
114
+ if (headers) config.headers = headers;
115
+ return config;
116
+ }
117
+
118
+ function redactRecord(
119
+ record: Record<string, string> | undefined,
120
+ redactAllValues: boolean,
121
+ ): Record<string, string> | undefined {
122
+ if (!record) return undefined;
123
+ return Object.fromEntries(
124
+ Object.entries(record).map(([key, value]) => [
125
+ key,
126
+ redactAllValues || SENSITIVE_KEY_PATTERN.test(key) ? REDACTED : value,
127
+ ]),
128
+ );
129
+ }
130
+
131
+ export function redactMCPServerConfig(config: MCPServerConfig): MCPServerConfig {
132
+ const redacted = { ...config } as MCPServerConfig;
133
+ if ("env" in redacted) {
134
+ const env = redactRecord(redacted.env, true);
135
+ if (env) redacted.env = env;
136
+ }
137
+ if ("headers" in redacted) {
138
+ const headers = redactRecord(redacted.headers, true);
139
+ if (headers) redacted.headers = headers;
140
+ }
141
+ if (redacted.auth) {
142
+ redacted.auth = {
143
+ type: redacted.auth.type,
144
+ credentialId: redacted.auth.credentialId ? REDACTED : undefined,
145
+ tokenUrl: redacted.auth.tokenUrl,
146
+ clientId: redacted.auth.clientId ? REDACTED : undefined,
147
+ clientSecret: redacted.auth.clientSecret ? REDACTED : undefined,
148
+ };
149
+ }
150
+ if (redacted.oauth) {
151
+ redacted.oauth = {
152
+ clientId: redacted.oauth.clientId ? REDACTED : undefined,
153
+ clientSecret: redacted.oauth.clientSecret ? REDACTED : undefined,
154
+ redirectUri: redacted.oauth.redirectUri,
155
+ callbackPort: redacted.oauth.callbackPort,
156
+ callbackPath: redacted.oauth.callbackPath,
157
+ };
158
+ }
159
+ return redacted;
160
+ }
161
+
162
+ function collectEntries(config: MCPConfigFile): RedactedServerEntry[] {
163
+ return Object.entries(config.mcpServers ?? {})
164
+ .sort(([a], [b]) => a.localeCompare(b))
165
+ .map(([name, serverConfig]) => ({ name, config: redactMCPServerConfig(serverConfig) }));
166
+ }
167
+
168
+ function writeJson(value: unknown): void {
169
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
170
+ }
171
+
172
+ function renderServerLine(entry: RedactedServerEntry): string {
173
+ const config = entry.config;
174
+ if (config.type === "http" || config.type === "sse") {
175
+ return `${entry.name}\t${config.type}\t${config.url}`;
176
+ }
177
+ const args = config.args && config.args.length > 0 ? ` ${config.args.join(" ")}` : "";
178
+ return `${entry.name}\tstdio\t${config.command}${args}`;
179
+ }
180
+
181
+ function renderDetails(entry: RedactedServerEntry): string {
182
+ return `${renderServerLine(entry)}\n${JSON.stringify(entry.config, null, 2)}`;
183
+ }
184
+
185
+ async function runAdd(args: MCPCommandArgs, scoped: ScopedPath): Promise<void> {
186
+ if (!args.name) throw new MCPArgsError("`gjc mcp add` requires a server name.");
187
+ const config = buildServerConfig(args);
188
+ const result = await upsertMCPServer(scoped.path, args.name, config, { force: args.flags.force });
189
+ const redacted = redactMCPServerConfig(config);
190
+ if (args.flags.json) {
191
+ writeJson({
192
+ action: "add",
193
+ status: result.status,
194
+ name: args.name,
195
+ scope: scoped.scope,
196
+ path: scoped.path,
197
+ config: redacted,
198
+ });
199
+ return;
200
+ }
201
+ if (result.status === "skipped") {
202
+ process.stdout.write(
203
+ `MCP server "${args.name}" already exists in ${scoped.scope} config. Pass --force to overwrite.\n`,
204
+ );
205
+ return;
206
+ }
207
+ process.stdout.write(`MCP server "${args.name}" ${result.status} in ${scoped.scope} config: ${scoped.path}\n`);
208
+ }
209
+
210
+ async function runList(args: MCPCommandArgs, scoped: ScopedPath): Promise<void> {
211
+ const config = await readMCPConfigFile(scoped.path);
212
+ const entries = collectEntries(config);
213
+ if (args.flags.json) {
214
+ writeJson({ action: "list", scope: scoped.scope, path: scoped.path, servers: entries });
215
+ return;
216
+ }
217
+ if (entries.length === 0) {
218
+ process.stdout.write(`No MCP servers registered in ${scoped.scope} config: ${scoped.path}\n`);
219
+ return;
220
+ }
221
+ process.stdout.write(`MCP servers in ${scoped.scope} config: ${scoped.path}\n`);
222
+ for (const entry of entries) {
223
+ process.stdout.write(`${renderDetails(entry)}\n`);
224
+ }
225
+ }
226
+
227
+ async function runRemove(args: MCPCommandArgs, scoped: ScopedPath): Promise<void> {
228
+ if (!args.name) throw new MCPArgsError("`gjc mcp remove` requires a server name.");
229
+ const existing = await getMCPServer(scoped.path, args.name);
230
+ if (!existing) {
231
+ throw new MCPArgsError(`MCP server "${args.name}" not found in ${scoped.scope} config.`);
232
+ }
233
+ await removeMCPServer(scoped.path, args.name);
234
+ const entry = { name: args.name, config: redactMCPServerConfig(existing) };
235
+ if (args.flags.json) {
236
+ writeJson({
237
+ action: "remove",
238
+ status: "removed",
239
+ name: args.name,
240
+ scope: scoped.scope,
241
+ path: scoped.path,
242
+ removed: entry,
243
+ });
244
+ return;
245
+ }
246
+ process.stdout.write(`Removed MCP server "${args.name}" from ${scoped.scope} config: ${scoped.path}\n`);
247
+ process.stdout.write(`${renderDetails(entry)}\n`);
248
+ }
249
+
250
+ export async function runMCPCommand(args: MCPCommandArgs): Promise<void> {
251
+ const scoped = resolvePath(args);
252
+ try {
253
+ switch (args.action) {
254
+ case "add":
255
+ await runAdd(args, scoped);
256
+ return;
257
+ case "list":
258
+ await runList(args, scoped);
259
+ return;
260
+ case "remove":
261
+ await runRemove(args, scoped);
262
+ return;
263
+ }
264
+ } catch (error) {
265
+ if (error instanceof MCPArgsError) {
266
+ process.stderr.write(`${error.message}\n`);
267
+ process.exitCode = 2;
268
+ return;
269
+ }
270
+ throw error;
271
+ }
272
+ }