@gajae-code/coding-agent 0.7.2 → 0.7.4

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 (154) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/bin/gjc.js +4 -0
  3. package/dist/types/cli/mcp-cli.d.ts +25 -0
  4. package/dist/types/cli/plugin-cli.d.ts +2 -0
  5. package/dist/types/cli.d.ts +6 -0
  6. package/dist/types/commands/mcp.d.ts +70 -0
  7. package/dist/types/commands/plugin.d.ts +6 -0
  8. package/dist/types/commands/session.d.ts +6 -0
  9. package/dist/types/config/keybindings.d.ts +2 -2
  10. package/dist/types/config/model-profile-activation.d.ts +8 -1
  11. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  12. package/dist/types/extensibility/gjc-plugins/compiler.d.ts +19 -0
  13. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +29 -0
  14. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  15. package/dist/types/extensibility/gjc-plugins/injection.d.ts +9 -0
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +13 -0
  17. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +26 -0
  18. package/dist/types/extensibility/gjc-plugins/observability.d.ts +27 -0
  19. package/dist/types/extensibility/gjc-plugins/prompt-appendix.d.ts +16 -0
  20. package/dist/types/extensibility/gjc-plugins/registry.d.ts +32 -0
  21. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +64 -0
  22. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +42 -0
  23. package/dist/types/extensibility/gjc-plugins/types.d.ts +158 -2
  24. package/dist/types/extensibility/gjc-plugins/validation.d.ts +8 -1
  25. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  26. package/dist/types/gjc-runtime/psmux-detect.d.ts +78 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -0
  28. package/dist/types/gjc-runtime/tmux-common.d.ts +20 -1
  29. package/dist/types/gjc-runtime/tmux-sessions.d.ts +18 -0
  30. package/dist/types/main.d.ts +2 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  32. package/dist/types/modes/components/model-selector.d.ts +8 -0
  33. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  34. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  35. package/dist/types/notifications/html-format.d.ts +11 -0
  36. package/dist/types/notifications/index.d.ts +149 -1
  37. package/dist/types/notifications/lifecycle-commands.d.ts +72 -0
  38. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  39. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  40. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  41. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  42. package/dist/types/notifications/recent-activity.d.ts +35 -0
  43. package/dist/types/notifications/telegram-daemon.d.ts +114 -16
  44. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  45. package/dist/types/notifications/topic-registry.d.ts +12 -9
  46. package/dist/types/runtime-mcp/types.d.ts +7 -0
  47. package/dist/types/sdk.d.ts +2 -0
  48. package/dist/types/session/agent-session.d.ts +14 -4
  49. package/dist/types/session/blob-store.d.ts +25 -0
  50. package/dist/types/session/session-manager.d.ts +57 -0
  51. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  52. package/dist/types/system-prompt.d.ts +2 -0
  53. package/dist/types/task/executor.d.ts +9 -1
  54. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  55. package/dist/types/tools/index.d.ts +3 -1
  56. package/dist/types/utils/changelog.d.ts +1 -0
  57. package/dist/types/web/insane/url-guard.d.ts +6 -3
  58. package/dist/types/web/scrapers/types.d.ts +5 -0
  59. package/dist/types/web/scrapers/utils.d.ts +7 -1
  60. package/package.json +11 -9
  61. package/scripts/g004-tmux-smoke.ts +100 -0
  62. package/scripts/g005-daemon-smoke.ts +181 -0
  63. package/scripts/g011-daemon-path-smoke.ts +153 -0
  64. package/src/cli/mcp-cli.ts +272 -0
  65. package/src/cli/plugin-cli.ts +66 -3
  66. package/src/cli.ts +27 -6
  67. package/src/commands/mcp.ts +117 -0
  68. package/src/commands/plugin.ts +4 -0
  69. package/src/commands/session.ts +18 -0
  70. package/src/config/keybindings.ts +2 -2
  71. package/src/config/model-profile-activation.ts +55 -7
  72. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  73. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +1 -1
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +7 -6
  75. package/src/defaults/gjc/skills/team/SKILL.md +5 -3
  76. package/src/defaults/gjc/skills/ultragoal/SKILL.md +41 -13
  77. package/src/export/html/index.ts +2 -2
  78. package/src/extensibility/extensions/runner.ts +1 -0
  79. package/src/extensibility/gjc-plugins/compiler.ts +351 -0
  80. package/src/extensibility/gjc-plugins/constrained-hooks.ts +170 -0
  81. package/src/extensibility/gjc-plugins/index.ts +9 -0
  82. package/src/extensibility/gjc-plugins/injection.ts +109 -0
  83. package/src/extensibility/gjc-plugins/installer.ts +434 -0
  84. package/src/extensibility/gjc-plugins/loader.ts +3 -1
  85. package/src/extensibility/gjc-plugins/mcp-policy.ts +239 -0
  86. package/src/extensibility/gjc-plugins/observability.ts +84 -0
  87. package/src/extensibility/gjc-plugins/paths.ts +1 -1
  88. package/src/extensibility/gjc-plugins/prompt-appendix.ts +109 -0
  89. package/src/extensibility/gjc-plugins/registry.ts +180 -0
  90. package/src/extensibility/gjc-plugins/runtime-adapters.ts +234 -0
  91. package/src/extensibility/gjc-plugins/schema.ts +250 -20
  92. package/src/extensibility/gjc-plugins/session-validation.ts +147 -0
  93. package/src/extensibility/gjc-plugins/types.ts +199 -3
  94. package/src/extensibility/gjc-plugins/validation.ts +80 -0
  95. package/src/extensibility/skills.ts +15 -0
  96. package/src/gjc-runtime/launch-tmux.ts +61 -7
  97. package/src/gjc-runtime/psmux-detect.ts +239 -0
  98. package/src/gjc-runtime/team-runtime.ts +56 -23
  99. package/src/gjc-runtime/tmux-common.ts +30 -3
  100. package/src/gjc-runtime/tmux-sessions.ts +51 -1
  101. package/src/gjc-runtime/ultragoal-guard.ts +25 -8
  102. package/src/gjc-runtime/ultragoal-runtime.ts +75 -15
  103. package/src/hooks/skill-state.ts +57 -0
  104. package/src/internal-urls/docs-index.generated.ts +12 -8
  105. package/src/main.ts +14 -3
  106. package/src/modes/bridge/bridge-mode.ts +11 -0
  107. package/src/modes/components/custom-editor.ts +2 -0
  108. package/src/modes/components/footer.ts +2 -3
  109. package/src/modes/components/hook-editor.ts +1 -1
  110. package/src/modes/components/hook-selector.ts +67 -43
  111. package/src/modes/components/model-selector.ts +56 -11
  112. package/src/modes/components/status-line/git-utils.ts +25 -0
  113. package/src/modes/components/status-line.ts +10 -11
  114. package/src/modes/components/welcome.ts +2 -3
  115. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  116. package/src/modes/controllers/selector-controller.ts +53 -11
  117. package/src/modes/interactive-mode.ts +4 -1
  118. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  119. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  120. package/src/modes/theme/defaults/index.ts +2 -0
  121. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  122. package/src/notifications/html-format.ts +38 -0
  123. package/src/notifications/index.ts +242 -12
  124. package/src/notifications/lifecycle-commands.ts +228 -0
  125. package/src/notifications/lifecycle-control-runtime.ts +400 -0
  126. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  127. package/src/notifications/operator-runtime.ts +171 -0
  128. package/src/notifications/rate-limit-pool.ts +19 -0
  129. package/src/notifications/recent-activity.ts +132 -0
  130. package/src/notifications/telegram-daemon.ts +778 -257
  131. package/src/notifications/telegram-reference.ts +25 -7
  132. package/src/notifications/topic-registry.ts +23 -9
  133. package/src/prompts/agents/executor.md +2 -2
  134. package/src/runtime-mcp/transports/stdio.ts +38 -4
  135. package/src/runtime-mcp/types.ts +7 -0
  136. package/src/sdk.ts +157 -10
  137. package/src/session/agent-session.ts +166 -74
  138. package/src/session/blob-store.ts +196 -8
  139. package/src/session/session-manager.ts +678 -7
  140. package/src/slash-commands/builtin-registry.ts +23 -3
  141. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  142. package/src/slash-commands/helpers/parse.ts +2 -1
  143. package/src/system-prompt.ts +9 -0
  144. package/src/task/executor.ts +31 -7
  145. package/src/task/index.ts +2 -0
  146. package/src/tools/ask.ts +5 -1
  147. package/src/tools/bash.ts +9 -0
  148. package/src/tools/composer-bash-policy.ts +96 -0
  149. package/src/tools/fetch.ts +18 -2
  150. package/src/tools/index.ts +3 -1
  151. package/src/utils/changelog.ts +8 -0
  152. package/src/web/insane/url-guard.ts +18 -14
  153. package/src/web/scrapers/types.ts +143 -45
  154. package/src/web/scrapers/utils.ts +70 -19
@@ -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
+ }
@@ -7,6 +7,7 @@
7
7
  import { APP_NAME, getProjectDir } from "@gajae-code/utils";
8
8
  import chalk from "chalk";
9
9
  import { resolveOrDefaultProjectRegistryPath } from "../discovery/helpers";
10
+ import { installGjcPluginBundle, isGjcPluginBundleSource, readRegistry } from "../extensibility/gjc-plugins";
10
11
  import { PluginManager, parseSettingValue, validateSetting } from "../extensibility/plugins";
11
12
  import {
12
13
  getInstalledPluginsRegistryPath,
@@ -48,6 +49,8 @@ export interface PluginCommandArgs {
48
49
  disable?: string;
49
50
  set?: string;
50
51
  scope?: "user" | "project";
52
+ user?: boolean;
53
+ project?: boolean;
51
54
  };
52
55
  }
53
56
 
@@ -109,6 +112,10 @@ export function parsePluginArgs(args: string[]): PluginCommandArgs | undefined {
109
112
  result.flags.dryRun = true;
110
113
  } else if (arg === "-l" || arg === "--local") {
111
114
  result.flags.local = true;
115
+ } else if (arg === "--user") {
116
+ result.flags.user = true;
117
+ } else if (arg === "--project") {
118
+ result.flags.project = true;
112
119
  } else if (arg === "--enable" && i + 1 < args.length) {
113
120
  result.flags.enable = args[++i];
114
121
  } else if (arg === "--disable" && i + 1 < args.length) {
@@ -345,7 +352,14 @@ async function handleUpgrade(args: string[], flags: PluginCommandArgs["flags"]):
345
352
  async function handleInstall(
346
353
  manager: PluginManager,
347
354
  packages: string[],
348
- flags: { json?: boolean; force?: boolean; dryRun?: boolean; scope?: "user" | "project" },
355
+ flags: {
356
+ json?: boolean;
357
+ force?: boolean;
358
+ dryRun?: boolean;
359
+ scope?: "user" | "project";
360
+ user?: boolean;
361
+ project?: boolean;
362
+ },
349
363
  ): Promise<void> {
350
364
  if (packages.length === 0) {
351
365
  console.error(chalk.red(`Usage: ${APP_NAME} plugin install <package[@version]>[features] ...`));
@@ -360,6 +374,32 @@ async function handleInstall(
360
374
  const knownMarketplaces = new Set((await mktMgr.listMarketplaces()).map(m => m.name));
361
375
 
362
376
  for (const spec of packages) {
377
+ // GJC plugin bundle classifier: a source containing gajae-plugin.json (or a
378
+ // git/tarball source) routes to the bundle installer BEFORE marketplace/npm.
379
+ if (await isGjcPluginBundleSource(spec)) {
380
+ if (flags.user === flags.project) {
381
+ console.error(
382
+ chalk.red(`GJC plugin bundle install requires exactly one of --user or --project for "${spec}".`),
383
+ );
384
+ process.exit(1);
385
+ }
386
+ const scope: "user" | "project" = flags.user ? "user" : "project";
387
+ try {
388
+ const res = await installGjcPluginBundle(spec, { scope, cwd: process.cwd(), force: flags.force });
389
+ if (flags.json) {
390
+ console.log(JSON.stringify({ name: res.entry.name, status: res.status, scope }, null, 2));
391
+ } else {
392
+ console.log(
393
+ chalk.green(`${theme.status.success} ${res.status} GJC plugin ${res.entry.name} (${scope})`),
394
+ );
395
+ }
396
+ } catch (err) {
397
+ console.error(chalk.red(`${theme.status.error} Failed to install GJC plugin ${spec}: ${err}`));
398
+ process.exit(1);
399
+ }
400
+ continue;
401
+ }
402
+
363
403
  const target = classifyInstallTarget(spec, knownMarketplaces);
364
404
 
365
405
  if (target.type === "marketplace") {
@@ -462,13 +502,16 @@ async function handleList(manager: PluginManager, flags: { json?: boolean }): Pr
462
502
  const npmPlugins = await manager.list();
463
503
  const mktMgr = await makeMarketplaceManager();
464
504
  const mktPlugins = await mktMgr.listInstalledPlugins();
505
+ const cwd = getProjectDir();
506
+ const [gjcUser, gjcProject] = await Promise.all([readRegistry("user", cwd), readRegistry("project", cwd)]);
507
+ const gjcBundles = [...gjcUser.plugins, ...gjcProject.plugins];
465
508
 
466
509
  if (flags.json) {
467
- console.log(JSON.stringify({ npm: npmPlugins, marketplace: mktPlugins }, null, 2));
510
+ console.log(JSON.stringify({ npm: npmPlugins, marketplace: mktPlugins, gjc: gjcBundles }, null, 2));
468
511
  return;
469
512
  }
470
513
 
471
- if (npmPlugins.length === 0 && mktPlugins.length === 0) {
514
+ if (npmPlugins.length === 0 && mktPlugins.length === 0 && gjcBundles.length === 0) {
472
515
  console.log(chalk.dim("No plugins installed"));
473
516
  console.log(chalk.dim(`\nInstall plugins with: ${APP_NAME} plugin install <package>`));
474
517
  return;
@@ -510,6 +553,26 @@ async function handleList(manager: PluginManager, flags: { json?: boolean }): Pr
510
553
  console.log(` ${plugin.id} (${version})${scopeLabel}${shadowLabel}`);
511
554
  }
512
555
  }
556
+
557
+ if (gjcBundles.length > 0) {
558
+ if (npmPlugins.length > 0 || mktPlugins.length > 0) console.log();
559
+ console.log(chalk.bold("GJC Plugin Bundles:\n"));
560
+ for (const plugin of gjcBundles) {
561
+ const status = plugin.enabled ? chalk.green(theme.status.enabled) : chalk.dim(theme.status.disabled);
562
+ const scopeLabel = chalk.dim(` (${plugin.scope})`);
563
+ const disabledCount = plugin.disabledSurfaceIds.length;
564
+ const quarantineCount = plugin.quarantine?.length ?? 0;
565
+ const detail = [
566
+ disabledCount > 0 ? `${disabledCount} disabled` : null,
567
+ quarantineCount > 0 ? `${quarantineCount} quarantined` : null,
568
+ ]
569
+ .filter((v): v is string => Boolean(v))
570
+ .join(", ");
571
+ console.log(
572
+ `${status} ${plugin.name}@${plugin.version}${scopeLabel}${detail ? chalk.dim(` — ${detail}`) : ""}`,
573
+ );
574
+ }
575
+ }
513
576
  }
514
577
 
515
578
  async function handleLink(manager: PluginManager, paths: string[], flags: { json?: boolean }): Promise<void> {
package/src/cli.ts CHANGED
@@ -22,7 +22,7 @@ process.title = APP_NAME;
22
22
  const rootHelpFlags = ["--help", "-h", "help"];
23
23
  const versionFlags = ["--version", "-v"];
24
24
 
25
- const commands: CommandEntry[] = [
25
+ export const commands: CommandEntry[] = [
26
26
  { name: "codex-native-hook", load: () => import("./commands/codex-native-hook").then(m => m.default) },
27
27
  { name: "state", load: () => import("./commands/state").then(m => m.default) },
28
28
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
@@ -39,6 +39,7 @@ const commands: CommandEntry[] = [
39
39
  { name: "daemon", load: () => import("./commands/daemon").then(m => m.default) },
40
40
  { name: "web-search", aliases: ["q"], load: () => import("./commands/web-search").then(m => m.default) },
41
41
  { name: "mcp-serve", load: () => import("./commands/mcp-serve").then(m => m.default) },
42
+ { name: "mcp", load: () => import("./commands/mcp").then(m => m.default) },
42
43
  {
43
44
  name: "contribute-pr",
44
45
  aliases: ["contribution-prep"],
@@ -48,6 +49,7 @@ const commands: CommandEntry[] = [
48
49
  { name: "migrate", load: () => import("./commands/migrate").then(m => m.default) },
49
50
  { name: "rlm", load: () => import("./commands/rlm").then(m => m.default) },
50
51
  { name: "update", load: () => import("./commands/update").then(m => m.default) },
52
+ { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
51
53
  { name: "launch", load: () => import("./commands/launch").then(m => m.default) },
52
54
  ];
53
55
 
@@ -62,7 +64,7 @@ async function showHelp(config: CliConfig): Promise<void> {
62
64
  }
63
65
 
64
66
  async function installRuntimeGlobals(): Promise<void> {
65
- const [{ installH2Fetch }, { procmgr }] = await Promise.all([import("@gajae-code/ai"), import("@gajae-code/utils")]);
67
+ const { installH2Fetch } = await import("@gajae-code/ai/utils/h2-fetch");
66
68
  // Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
67
69
  // discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
68
70
  // can't toggle from JS, so we patch globalThis.fetch to pass
@@ -73,7 +75,24 @@ async function installRuntimeGlobals(): Promise<void> {
73
75
  // Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
74
76
  // Otherwise every child bun process (subagents, plugin installs, ptree spawns,
75
77
  // etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
76
- procmgr.scrubProcessEnv();
78
+ delete process.env.MallocStackLogging;
79
+ delete process.env.MallocStackLoggingNoCompact;
80
+ }
81
+
82
+ function hasRootFastFlag(argv: string[], flags: readonly string[]): boolean {
83
+ for (const arg of argv) {
84
+ if (isSubcommand(arg)) return false;
85
+ if (flags.includes(arg)) return true;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ function hasRootHelpFlag(argv: string[]): boolean {
91
+ return hasRootFastFlag(argv, rootHelpFlags);
92
+ }
93
+
94
+ function hasRootVersionFlag(argv: string[]): boolean {
95
+ return hasRootFastFlag(argv, versionFlags);
77
96
  }
78
97
 
79
98
  class RootHelpCommand extends Command {
@@ -193,7 +212,7 @@ export async function runCli(argv: string[]): Promise<void> {
193
212
  await runSmokeTest();
194
213
  return;
195
214
  }
196
- if (rootHelpFlags.includes(argv[0] ?? "")) {
215
+ if (hasRootHelpFlag(argv)) {
197
216
  const { renderRootHelp } = await import("@gajae-code/utils/cli");
198
217
  const { getExtraHelpText } = await import("./cli/fast-help");
199
218
  renderRootHelp({ bin: APP_NAME, version: VERSION, commands: new Map([["launch", RootHelpCommand]]) });
@@ -203,7 +222,7 @@ export async function runCli(argv: string[]): Promise<void> {
203
222
  }
204
223
  return;
205
224
  }
206
- if (versionFlags.includes(argv[0] ?? "")) {
225
+ if (hasRootVersionFlag(argv)) {
207
226
  process.stdout.write(`${APP_NAME}/${VERSION}\n`);
208
227
  return;
209
228
  }
@@ -220,4 +239,6 @@ export async function runCli(argv: string[]): Promise<void> {
220
239
  return run({ bin: APP_NAME, version: VERSION, argv: runArgv, commands, help: showHelp });
221
240
  }
222
241
 
223
- await runCli(process.argv.slice(2));
242
+ if (import.meta.main) {
243
+ await runCli(process.argv.slice(2));
244
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Direct MCP server registration for standalone GJC.
3
+ */
4
+ import { Args, Command, Flags } from "@gajae-code/utils/cli";
5
+ import { type MCPAction, type MCPCommandArgs, runMCPCommand } from "../cli/mcp-cli";
6
+
7
+ const ACTIONS: MCPAction[] = ["add", "list", "remove"];
8
+
9
+ export default class MCP extends Command {
10
+ static description = "Register standalone MCP servers explicitly in GJC config";
11
+ static delegateHelp = true;
12
+
13
+ static examples = [
14
+ "gjc mcp add context7 npx -y @upstash/context7-mcp",
15
+ "gjc mcp add docs --type http --url https://example.test/mcp --header Authorization=Bearer_TOKEN",
16
+ "gjc mcp list --json",
17
+ "gjc mcp remove context7",
18
+ ];
19
+
20
+ static args = {
21
+ action: Args.string({ description: "MCP action", required: false, options: ACTIONS }),
22
+ name: Args.string({ description: "Server name", required: false }),
23
+ commandArgs: Args.string({
24
+ description: "Command/URL and trailing args for add",
25
+ required: false,
26
+ multiple: true,
27
+ }),
28
+ };
29
+
30
+ static flags = {
31
+ project: Flags.boolean({ description: "Write/read project scope (./.gjc/mcp.json) instead of user scope" }),
32
+ force: Flags.boolean({ description: "Overwrite an existing server during add", default: false }),
33
+ json: Flags.boolean({
34
+ char: "j",
35
+ description: "Emit machine-readable JSON with sensitive values redacted",
36
+ default: false,
37
+ }),
38
+ type: Flags.string({ description: "Server transport type", options: ["stdio", "http", "sse"] }),
39
+ command: Flags.string({ description: "Stdio server command for add" }),
40
+ url: Flags.string({ description: "HTTP/SSE server URL for add" }),
41
+ arg: Flags.string({ description: "Argument passed to a stdio server (repeatable)", multiple: true }),
42
+ env: Flags.string({
43
+ description: "Environment variable for stdio server as KEY=VALUE (repeatable)",
44
+ multiple: true,
45
+ }),
46
+ header: Flags.string({
47
+ description: "HTTP/SSE header as KEY=VALUE (repeatable; redacted in output)",
48
+ multiple: true,
49
+ }),
50
+ cwd: Flags.string({ description: "Working directory for stdio server" }),
51
+ timeout: Flags.integer({ description: "Connection timeout in milliseconds" }),
52
+ };
53
+
54
+ async run(): Promise<void> {
55
+ if (this.argv.includes("--help") || this.argv.includes("-h")) {
56
+ this.printHelp();
57
+ return;
58
+ }
59
+
60
+ const { args, flags } = await this.parse(MCP);
61
+ const action = (args.action ?? "list") as MCPAction;
62
+ const cmd: MCPCommandArgs = {
63
+ action,
64
+ name: args.name,
65
+ commandArgs: args.commandArgs,
66
+ flags: {
67
+ project: flags.project,
68
+ force: flags.force,
69
+ json: flags.json,
70
+ type: flags.type as MCPCommandArgs["flags"]["type"],
71
+ command: flags.command,
72
+ url: flags.url,
73
+ arg: flags.arg,
74
+ env: flags.env,
75
+ header: flags.header,
76
+ cwd: flags.cwd,
77
+ timeout: flags.timeout,
78
+ },
79
+ };
80
+ await runMCPCommand(cmd);
81
+ }
82
+
83
+ private printHelp(): void {
84
+ process.stdout.write(`Register standalone MCP servers explicitly in GJC config
85
+
86
+ USAGE
87
+ $ gjc mcp [add|list|remove] [NAME] [COMMAND_OR_URL] [ARGS...] [FLAGS]
88
+
89
+ COMMANDS
90
+ add Add an explicit user-provided MCP server definition
91
+ list List registered servers with env/header/auth values redacted
92
+ remove Remove a registered server and print the removed definition redacted
93
+
94
+ FLAGS
95
+ --project Use project scope (./.gjc/mcp.json) instead of user scope
96
+ --force Overwrite an existing server during add
97
+ -j, --json Emit machine-readable JSON with sensitive values redacted
98
+ --type=<value> stdio | http | sse (default: stdio, or http when --url is set)
99
+ --command=<value> Stdio server command for add
100
+ --url=<value> HTTP/SSE server URL for add
101
+ --arg=<value> Stdio server argument (repeatable)
102
+ --env=<value> Stdio env var as KEY=VALUE (repeatable; redacted in output)
103
+ --header=<value> HTTP/SSE header as KEY=VALUE (repeatable; redacted in output)
104
+ --cwd=<value> Working directory for stdio server
105
+ --timeout=<int> Connection timeout in milliseconds
106
+
107
+ EXAMPLES
108
+ $ gjc mcp add context7 npx -y @upstash/context7-mcp
109
+ $ gjc mcp add docs --type http --url https://example.test/mcp --header Authorization=Bearer_TOKEN
110
+ $ gjc mcp list --json
111
+ $ gjc mcp remove context7
112
+
113
+ SECURITY
114
+ This command writes only the server definition supplied on this invocation. It does not import or inherit Claude Code, Codex, OpenCode, or other live MCP configs. Public output redacts env, header, auth, and OAuth credential values.
115
+ `);
116
+ }
117
+ }
@@ -49,6 +49,8 @@ export default class Plugin extends Command {
49
49
  description: 'Install scope: "user" (default) or "project"',
50
50
  options: ["user", "project"],
51
51
  }),
52
+ user: Flags.boolean({ description: "Install GJC plugin bundle into the user root" }),
53
+ project: Flags.boolean({ description: "Install GJC plugin bundle into the project root" }),
52
54
  };
53
55
 
54
56
  async run(): Promise<void> {
@@ -69,6 +71,8 @@ export default class Plugin extends Command {
69
71
  disable: flags.disable,
70
72
  set: flags.set,
71
73
  scope: flags.scope as "user" | "project" | undefined,
74
+ user: flags.user,
75
+ project: flags.project,
72
76
  },
73
77
  };
74
78
 
@@ -2,6 +2,7 @@ import { Args, Command, Flags } from "@gajae-code/utils/cli";
2
2
  import {
3
3
  attachGjcTmuxSession,
4
4
  createGjcTmuxSession,
5
+ forceCloseGjcTmuxSession,
5
6
  listGjcTmuxSessions,
6
7
  removeGjcTmuxSession,
7
8
  statusGjcTmuxSession,
@@ -60,6 +61,12 @@ export default class Session extends Command {
60
61
 
61
62
  static flags = {
62
63
  json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: false }),
64
+ "session-id": Flags.string({
65
+ description: "Expected @gjc-session-id tag for force-close (defense-in-depth match)",
66
+ }),
67
+ "state-file": Flags.string({
68
+ description: "Expected @gjc-session-state-file tag for force-close (defense-in-depth match)",
69
+ }),
63
70
  };
64
71
 
65
72
  static examples = [
@@ -68,6 +75,7 @@ export default class Session extends Command {
68
75
  "gjc session status <session>",
69
76
  "gjc session attach <session>",
70
77
  "gjc session remove <session>",
78
+ "gjc session force-close <session> --session-id <id>",
71
79
  ];
72
80
 
73
81
  async run(): Promise<void> {
@@ -136,6 +144,16 @@ export default class Session extends Command {
136
144
  return;
137
145
  }
138
146
 
147
+ if (action === "force-close" || action === "force-remove") {
148
+ const closed = forceCloseGjcTmuxSession(sessionName, process.env, flags["session-id"], flags["state-file"]);
149
+ if (json) {
150
+ writeJson({ ok: true, session: sessionJson(closed) });
151
+ return;
152
+ }
153
+ writeText([`force-closed: ${closed.name}`]);
154
+ return;
155
+ }
156
+
139
157
  if (action === "attach") {
140
158
  attachGjcTmuxSession(sessionName);
141
159
  return;
@@ -117,8 +117,8 @@ export const KEYBINDINGS = {
117
117
  description: "Open external editor",
118
118
  },
119
119
  "app.message.followUp": {
120
- defaultKeys: "ctrl+enter",
121
- description: "Send follow-up message",
120
+ defaultKeys: [],
121
+ description: "Send follow-up message (no default; Ctrl+Enter inserts a newline)",
122
122
  },
123
123
  "app.message.queue": {
124
124
  defaultKeys: "alt+enter",