@gajae-code/coding-agent 0.7.2 → 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.
- package/CHANGELOG.md +38 -0
- package/dist/types/cli/mcp-cli.d.ts +25 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/mcp.d.ts +70 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
- package/dist/types/modes/theme/defaults/index.d.ts +99 -0
- package/dist/types/notifications/operator-runtime.d.ts +52 -0
- package/dist/types/notifications/telegram-daemon.d.ts +54 -16
- package/dist/types/notifications/topic-registry.d.ts +2 -0
- package/dist/types/tools/composer-bash-policy.d.ts +14 -0
- package/dist/types/web/insane/url-guard.d.ts +6 -3
- package/dist/types/web/scrapers/types.d.ts +5 -0
- package/dist/types/web/scrapers/utils.d.ts +7 -1
- package/package.json +7 -7
- package/src/cli/mcp-cli.ts +272 -0
- package/src/cli.ts +6 -2
- package/src/commands/mcp.ts +117 -0
- package/src/config/keybindings.ts +2 -2
- package/src/deep-interview/plaintext-gate-guard.ts +94 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +4 -3
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/gjc-runtime/tmux-common.ts +3 -1
- package/src/gjc-runtime/ultragoal-guard.ts +25 -8
- package/src/hooks/skill-state.ts +57 -0
- package/src/internal-urls/docs-index.generated.ts +10 -7
- package/src/modes/bridge/bridge-mode.ts +11 -0
- package/src/modes/components/custom-editor.ts +2 -0
- package/src/modes/components/footer.ts +2 -3
- package/src/modes/components/model-selector.ts +12 -0
- package/src/modes/components/status-line/git-utils.ts +25 -0
- package/src/modes/components/status-line.ts +10 -11
- package/src/modes/components/welcome.ts +2 -3
- package/src/modes/controllers/selector-controller.ts +3 -0
- package/src/modes/interactive-mode.ts +2 -1
- package/src/modes/shared/agent-wire/scopes.ts +1 -1
- package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/notifications/operator-runtime.ts +171 -0
- package/src/notifications/telegram-daemon.ts +347 -251
- package/src/notifications/topic-registry.ts +5 -0
- package/src/slash-commands/helpers/parse.ts +2 -1
- package/src/tools/bash.ts +9 -0
- package/src/tools/composer-bash-policy.ts +96 -0
- package/src/tools/fetch.ts +18 -2
- package/src/web/insane/url-guard.ts +18 -14
- package/src/web/scrapers/types.ts +143 -45
- 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
|
+
}
|
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
|
|
|
@@ -220,4 +222,6 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
220
222
|
return run({ bin: APP_NAME, version: VERSION, argv: runArgv, commands, help: showHelp });
|
|
221
223
|
}
|
|
222
224
|
|
|
223
|
-
|
|
225
|
+
if (import.meta.main) {
|
|
226
|
+
await runCli(process.argv.slice(2));
|
|
227
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -117,8 +117,8 @@ export const KEYBINDINGS = {
|
|
|
117
117
|
description: "Open external editor",
|
|
118
118
|
},
|
|
119
119
|
"app.message.followUp": {
|
|
120
|
-
defaultKeys:
|
|
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",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export type DeepInterviewPlaintextAskLeakOption = "Yes, crystallize" | "Adjust wording" | "Missing scope";
|
|
2
|
+
|
|
3
|
+
export type DeepInterviewPlaintextAskLeakResult = {
|
|
4
|
+
kind: "deep_interview_plaintext_ask_leak";
|
|
5
|
+
matchedOptions: DeepInterviewPlaintextAskLeakOption[];
|
|
6
|
+
signals: {
|
|
7
|
+
optionsHeading: true;
|
|
8
|
+
restateIntent: true;
|
|
9
|
+
deepInterviewContext: boolean;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const CANONICAL_OPTIONS: DeepInterviewPlaintextAskLeakOption[] = [
|
|
14
|
+
"Yes, crystallize",
|
|
15
|
+
"Adjust wording",
|
|
16
|
+
"Missing scope",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const RESTATE_INTENT_PATTERNS: RegExp[] = [
|
|
20
|
+
/\brestate\b/i,
|
|
21
|
+
/\bconfirmation\b/i,
|
|
22
|
+
/\bconfirm(?:ation|ed|ing)?\b/i,
|
|
23
|
+
/\bcrystall?ize\b/i,
|
|
24
|
+
/\bdoes\s+this\s+(?:capture|match|reflect|summari[sz]e)\b/i,
|
|
25
|
+
/\bbefore\s+(?:i|we)\s+(?:write|finali[sz]e|crystall?ize)\b/i,
|
|
26
|
+
/\bread\s+only\s+this\s+line\b/i,
|
|
27
|
+
/\bturn\s+this\s+into\s+(?:the\s+)?(?:spec|final)\b/i,
|
|
28
|
+
/재진술/,
|
|
29
|
+
/다시\s*말/,
|
|
30
|
+
/확인/,
|
|
31
|
+
/확정/,
|
|
32
|
+
/정리/,
|
|
33
|
+
/요약/,
|
|
34
|
+
/맞(?:습니까|나요|는지)/,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const DEEP_INTERVIEW_CONTEXT_PATTERNS: RegExp[] = [
|
|
38
|
+
/\bdeep[ -]?interview\b/i,
|
|
39
|
+
/\bsocratic\b/i,
|
|
40
|
+
/\binterview\s+round\b/i,
|
|
41
|
+
/\bround\s+\d+\b/i,
|
|
42
|
+
/\bambiguity\b/i,
|
|
43
|
+
/\btopology\s+confirmation\b/i,
|
|
44
|
+
/딥\s*인터뷰/,
|
|
45
|
+
/심층\s*인터뷰/,
|
|
46
|
+
/인터뷰\s*(?:라운드|진행|확인)/,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const FINAL_ARTIFACT_PATTERNS: RegExp[] = [
|
|
50
|
+
/^\s*#{1,3}\s*(?:(?:final|approved|completed|완료|최종)\s+)?(?:deep[ -]?interview\s+)?(?:spec(?:ification)?|transcript)\b/im,
|
|
51
|
+
/^\s*#{1,3}\s*(?:deep[ -]?interview\s+)?(?:final|approved|completed|완료|최종)\s+(?:spec(?:ification)?|transcript)\b/im,
|
|
52
|
+
/\b(?:final|approved|completed)\s+(?:deep[ -]?interview\s+)?(?:spec(?:ification)?|transcript)\b/i,
|
|
53
|
+
/\binterview\s+transcript\b/i,
|
|
54
|
+
/\.gjc\/specs\//i,
|
|
55
|
+
/최종\s*(?:명세|스펙|기록)/,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function hasOptionsHeading(text: string): boolean {
|
|
59
|
+
return /(?:^|\n)\s*options\s*:/i.test(text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasPattern(text: string, patterns: RegExp[]): boolean {
|
|
63
|
+
return patterns.some(pattern => pattern.test(text));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function matchedCanonicalOptions(text: string): DeepInterviewPlaintextAskLeakOption[] {
|
|
67
|
+
const folded = text.toLocaleLowerCase("en-US");
|
|
68
|
+
return CANONICAL_OPTIONS.filter(option => folded.includes(option.toLocaleLowerCase("en-US")));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function looksLikeFinalArtifact(text: string): boolean {
|
|
72
|
+
return hasPattern(text, FINAL_ARTIFACT_PATTERNS);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function detectDeepInterviewPlaintextAskLeak(text: string): DeepInterviewPlaintextAskLeakResult | null {
|
|
76
|
+
const trimmed = text.trim();
|
|
77
|
+
if (trimmed.length === 0) return null;
|
|
78
|
+
if (looksLikeFinalArtifact(trimmed)) return null;
|
|
79
|
+
if (!hasOptionsHeading(trimmed)) return null;
|
|
80
|
+
|
|
81
|
+
const matchedOptions = matchedCanonicalOptions(trimmed);
|
|
82
|
+
if (matchedOptions.length < 2) return null;
|
|
83
|
+
if (!hasPattern(trimmed, RESTATE_INTENT_PATTERNS)) return null;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
kind: "deep_interview_plaintext_ask_leak",
|
|
87
|
+
matchedOptions,
|
|
88
|
+
signals: {
|
|
89
|
+
optionsHeading: true,
|
|
90
|
+
restateIntent: true,
|
|
91
|
+
deepInterviewContext: hasPattern(trimmed, DEEP_INTERVIEW_CONTEXT_PATTERNS),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -292,7 +292,7 @@ Auto-research must never add a public skill entrypoint, never be slash-command/d
|
|
|
292
292
|
|
|
293
293
|
### Step 2b: Ask the Question
|
|
294
294
|
|
|
295
|
-
Use the `ask` tool with the generated question. Before rendering the prompt/options, apply `language.instruction` from state when present so the entire user-facing question remains in the preserved session language. Present it clearly with the current ambiguity context:
|
|
295
|
+
Use the `ask` tool with the generated question. When a question has options, you MUST call `ask` and must not print `Question:`/`Options:` blocks as assistant prose. If you already printed a question/options block as prose, your next action is to call `ask` with the same question/options, not to wait for a typed answer. Before rendering the prompt/options, apply `language.instruction` from state when present so the entire user-facing question remains in the preserved session language. Present it clearly with the current ambiguity context:
|
|
296
296
|
|
|
297
297
|
```
|
|
298
298
|
Round {n} | Component: {target_component_name} | Targeting: {weakest_dimension} | Why now: {one_sentence_targeting_rationale} | Ambiguity: {score}%
|
|
@@ -495,7 +495,7 @@ When ambiguity ≤ threshold (or hard cap / early exit):
|
|
|
495
495
|
|
|
496
496
|
**4a. Closure / Acceptance Guard.** Even when ambiguity ≤ threshold, do not treat the math as completion. Run an independent readiness audit from the full main-session perspective (including explore findings, established facts, and triggers the scorer may not have fully weighed). Confirm every active topology component has goal/constraint/criteria coverage, no unresolved or disputed trigger remains on a path that matters, and no low-confidence auto-answer is standing in for user-confirmed truth above the clarity cap. If a material gap exists, explicitly override the gate to the user — "The math says ready, but I am not accepting it yet because {gap}" — and ask the single highest-impact follow-up, returning to Phase 2. Record any override in `state.closure_overrides`.
|
|
497
497
|
|
|
498
|
-
**4b. Restate gate.** Once closure passes, collapse the agreed answers into ONE sentence goal that covers every active component, and confirm it with a single `ask`: "If someone read only this line, would they reach the same outcome you have in mind?" Offer **Yes, crystallize**, **Adjust wording**, and **Missing scope**, plus free-text, applying `language.instruction` when present. On "Adjust wording" / "Missing scope", collect the exact correction with one follow-up `ask`, route it back through Step 2c scoring and established-facts maintenance (a correction can change ambiguity), then re-run closure and ask the Restate gate again. Cap at two loops; if alignment is not reached, return to Phase 2 with a targeted question instead of forcing a goal line. Persist the confirmed line as `state.restated_goal`.
|
|
498
|
+
**4b. Restate gate.** Once closure passes, collapse the agreed answers into ONE sentence goal that covers every active component, and confirm it with a single `ask`: "If someone read only this line, would they reach the same outcome you have in mind?" Offer **Yes, crystallize**, **Adjust wording**, and **Missing scope**, plus free-text, applying `language.instruction` when present. Because this gate has options, it MUST go through `ask`: do not print the Restate question and options as assistant prose with `Question:`/`Options:` labels. If the Restate gate was already printed that way, immediately call `ask` with the same question/options before accepting or waiting for any answer. On "Adjust wording" / "Missing scope", collect the exact correction with one follow-up `ask`, route it back through Step 2c scoring and established-facts maintenance (a correction can change ambiguity), then re-run closure and ask the Restate gate again. Cap at two loops; if alignment is not reached, return to Phase 2 with a targeted question instead of forcing a goal line. Persist the confirmed line as `state.restated_goal`.
|
|
499
499
|
|
|
500
500
|
1. **Generate the specification** using opus model with the prompt-safe transcript. If the full interview transcript or initial context is too large, include the summary plus all concrete decisions, acceptance criteria, unresolved gaps, and ontology snapshots; never overflow the prompt with raw oversized context.
|
|
501
501
|
- Apply `language.instruction` when present so user-facing prose in the spec preserves the session language; keep code identifiers, file paths, commands, JSON/settings keys, and quoted source text unchanged.
|
|
@@ -690,6 +690,7 @@ Skipping any stage is possible but reduces quality assurance:
|
|
|
690
690
|
|
|
691
691
|
<Tool_Usage>
|
|
692
692
|
- Use the `ask` tool for each interview question — provides clickable UI with contextual options
|
|
693
|
+
- For any option-bearing question, call `ask`; never print `Question:`/`Options:` blocks as assistant prose. If such a block was already printed, call `ask` with the same question/options as the very next action instead of waiting for a typed/prose answer
|
|
693
694
|
- Preserve the GJC `ask` tool path for native interaction; do not introduce parallel structured-question transport into this skill
|
|
694
695
|
- Use `read/search/find exploration or a bounded read-only planner/architect subagent` for brownfield codebase exploration (run BEFORE asking user about codebase)
|
|
695
696
|
- Use opus model (temperature 0.1) for ambiguity scoring — consistency is critical
|
|
@@ -698,7 +699,7 @@ Skipping any stage is possible but reduces quality assurance:
|
|
|
698
699
|
- Use the GJC workflow CLI to save the final spec at `.gjc/_session-{sessionid}/specs/deep-interview-{slug}.md` exactly; do not use `write`, `edit`, or `ast_edit` directly on `.gjc/` paths without force override.
|
|
699
700
|
- Use public GJC workflow entrypoints to bridge to ralplan, ultragoal, or team only after explicit execution approval — never implement directly. Implementation handoff defaults to ultragoal; reserve team for when tmux-based interactive worker parallelization is genuinely required.
|
|
700
701
|
- The lateral-review panel spawns read-only persona subagents (Task tool) in parallel with independent context; it is an assist layer, never an executor and never the completion authority
|
|
701
|
-
- Apply the Refine gate (Step 2b″), the Dialectic Rhythm Guard (Step 2a), and the Closure + Restate gates (Phase 4) through the `ask` tool, preserving `language.instruction` for each
|
|
702
|
+
- Apply the Refine gate (Step 2b″), the Dialectic Rhythm Guard (Step 2a), and the Closure + Restate gates (Phase 4) through the `ask` tool, preserving `language.instruction` for each; if any of these gates has options, the assistant must call `ask` and must not print `Question:`/`Options:` blocks as assistant prose
|
|
702
703
|
- Use internal fragment auto-modes only at their documented hooks: `auto-research-greenfield.md` between Step 2a and 2b for greenfield `research: true` questions, `auto-answer-uncertain.md` as Step 2b′ after `ask` resolves and before scoring, and `lateral-review-panel.md` for the Phase 3 panel personas at ambiguity-milestone transitions and before synthesizing agent-supplied answers.
|
|
703
704
|
- Fragment auto-modes are loaded on demand as `kind: "skill-fragment"`; they are not public workflow skills, not slash-command/discoverable, and not `skill://` registrations.
|
|
704
705
|
</Tool_Usage>
|
|
@@ -311,8 +311,9 @@ Worker protocol:
|
|
|
311
311
|
Useful runtime env vars:
|
|
312
312
|
|
|
313
313
|
- `GJC_TMUX_COMMAND` / `GJC_TEAM_TMUX_COMMAND`
|
|
314
|
-
- tmux binary/
|
|
315
|
-
-
|
|
314
|
+
- tmux binary/name override (default `tmux`). `GJC_TMUX_COMMAND` applies to every GJC tmux flow; `GJC_TEAM_TMUX_COMMAND` is honored as an alias by the team path. Both resolve through the same resolver, so the team leader and `gjc session ...` always target the same multiplexer. These values are executable path/name overrides, not shell command lines; do not include flags such as `psmux -L <namespace>` in the env var.
|
|
315
|
+
- Windows psmux namespace boundary: psmux can be exposed as `psmux.exe` or as its `tmux.exe`/`pmux.exe` aliases. Its `-c <path>` cwd/start-directory flags do not isolate the server namespace; psmux uses the tmux-compatible global `-L <namespace>` flag for isolated server instances. GJC does not currently expose structured runtime `-L` support, because launch, `gjc session`, and `gjc team` must all carry the same namespace prefix together.
|
|
316
|
+
- Multiplexer support boundary: GJC-managed sessions and the team leader are detected via tmux user options (`@gjc-profile`, written with `set-option` and read back with `show-options` / `list-sessions -F`). A provider must round-trip those user options to be supported. Real tmux works. Alternative multiplexers such as psmux on Windows do not reliably persist tmux user options yet, so `gjc session status` reports `gjc_tmux_session_untagged` (the session exists in the multiplexer but is not GJC-tagged) and team startup rejects the leader as `unmanaged_tmux_session`. psmux namespace isolation is separate from this ownership-tag support boundary; `-L` prevents cross-namespace server collision, but it does not make GJC-managed session and team flows supported while user options fail to round-trip. Use real tmux for GJC-managed session and team flows.
|
|
316
317
|
- `GJC_TEAM_WORKER_COMMAND`
|
|
317
318
|
- worker command override (default resolves to active GJC entrypoint or `gjc`)
|
|
318
319
|
- `GJC_TEAM_STATE_ROOT`
|
|
@@ -55,7 +55,9 @@ export function buildGjcTmuxUntaggedSessionHint(tmuxCommand: string): string {
|
|
|
55
55
|
return (
|
|
56
56
|
`the active multiplexer "${tmuxCommand}" lists this session but did not return GJC's ${GJC_TMUX_PROFILE_OPTION} ownership tag; ` +
|
|
57
57
|
"GJC-managed sessions and `gjc team` require a tmux provider that round-trips tmux user options. " +
|
|
58
|
-
"
|
|
58
|
+
"For psmux on Windows, cwd/start-directory flags such as `-c` do not isolate the server namespace; psmux uses the tmux-compatible global `-L <namespace>` flag for that. " +
|
|
59
|
+
"GJC_TMUX_COMMAND and GJC_TEAM_TMUX_COMMAND are binary overrides, not shell command lines, so `psmux -L name` is not a supported value. " +
|
|
60
|
+
"Alternative multiplexers such as psmux on Windows do not reliably persist user options yet, so the Windows-native psmux path is not fully supported; " +
|
|
59
61
|
"use real tmux for GJC-managed session and team flows."
|
|
60
62
|
);
|
|
61
63
|
}
|