mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 12:14:19 +00:00
1 parent
7e958784c4
commit
1b5376239f
4 files changed
+311
-38
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# CCS Project Roadmap
|
# CCS Project Roadmap
|
||||||
|
|
||||||
Last Updated: 2026-04-30
|
Last Updated: 2026-05-07
|
||||||
|
|
||||||
Forward-looking roadmap documenting current priorities, GitHub issues, and future feature plans.
|
Forward-looking roadmap documenting current priorities, GitHub issues, and future feature plans.
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ All major modularization work is complete. The codebase evolved from monolithic
|
|||||||
|
|
||||||
### Recent Fixes
|
### Recent Fixes
|
||||||
|
|
||||||
|
- **2026-05-07**: **#1189** Headless settings-profile delegation now preserves native Claude passthrough args without a Claude flag allowlist. Explicit `--channels` values reach Claude Code, future native flags can carry multiple adjacent values, malformed CCS-owned flags no longer swallow the next native flag, and `--prompt=<text>` routes through headless delegation consistently with `--prompt <text>`.
|
||||||
- **2026-05-03**: **#1172** Local CLIProxy config generation now keeps the CPAMC management dashboard aligned with backend selection. `backend: original` points to the upstream dashboard, `backend: plus` points to the CCS-maintained CPAMC fork, `cliproxy.management_panel_repository` lets advanced users override the panel repository, and stale generated configs are regenerated when the expected panel source changes.
|
- **2026-05-03**: **#1172** Local CLIProxy config generation now keeps the CPAMC management dashboard aligned with backend selection. `backend: original` points to the upstream dashboard, `backend: plus` points to the CCS-maintained CPAMC fork, `cliproxy.management_panel_repository` lets advanced users override the panel repository, and stale generated configs are regenerated when the expected panel source changes.
|
||||||
- **2026-04-30**: **#1153** Native Claude launches now accept session-scoped `--effort low|medium|high|xhigh|max` overrides through CCS without mutating global Claude settings. CCS validates invalid or missing effort values before spawning Claude, normalizes accepted values, keeps default headless `-p/--prompt` launches on native Claude instead of delegation parsing, and preserves CLIProxy/Codex/Droid effort aliases.
|
- **2026-04-30**: **#1153** Native Claude launches now accept session-scoped `--effort low|medium|high|xhigh|max` overrides through CCS without mutating global Claude settings. CCS validates invalid or missing effort values before spawning Claude, normalizes accepted values, keeps default headless `-p/--prompt` launches on native Claude instead of delegation parsing, and preserves CLIProxy/Codex/Droid effort aliases.
|
||||||
- **2026-04-28**: **#1123** CLIProxy quota failover now uses the dashboard/manual pause mechanism for all quota-visible OAuth providers with CCS quota fetchers: Antigravity, Claude, Codex, Gemini CLI, and GitHub Copilot. When a healthy fallback exists, CCS moves the exhausted account token out of the live `auth/` folder into `auth-paused/`, marks the account paused for dashboard visibility, persists the cooldown for auto-resume, and still avoids self-pausing the last usable account.
|
- **2026-04-28**: **#1123** CLIProxy quota failover now uses the dashboard/manual pause mechanism for all quota-visible OAuth providers with CCS quota fetchers: Antigravity, Claude, Codex, Gemini CLI, and GitHub Copilot. When a healthy fallback exists, CCS moves the exhausted account token out of the live `auth/` folder into `auth-paused/`, marks the account paused for dashboard visibility, persists the cooldown for auto-resume, and still avoids self-pausing the last usable account.
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ async function main(): Promise<void> {
|
|||||||
// Special case: headless delegation (-p/--prompt)
|
// Special case: headless delegation (-p/--prompt)
|
||||||
// Keep existing behavior for Claude targets only; non-claude targets must continue
|
// Keep existing behavior for Claude targets only; non-claude targets must continue
|
||||||
// through normal adapter dispatch logic.
|
// through normal adapter dispatch logic.
|
||||||
if (args.includes('-p') || args.includes('--prompt')) {
|
if (args.some((arg) => arg === '-p' || arg === '--prompt' || arg.startsWith('--prompt='))) {
|
||||||
const shouldUseDelegation = resolvedTarget === 'claude' && profileInfo.type === 'settings';
|
const shouldUseDelegation = resolvedTarget === 'claude' && profileInfo.type === 'settings';
|
||||||
if (shouldUseDelegation) {
|
if (shouldUseDelegation) {
|
||||||
const { DelegationHandler } = await import('./delegation/delegation-handler');
|
const { DelegationHandler } = await import('./delegation/delegation-handler');
|
||||||
|
|||||||
@@ -9,6 +9,59 @@ import { fail, warn } from '../utils/ui';
|
|||||||
import { getCcsDir } from '../config/config-loader-facade';
|
import { getCcsDir } from '../config/config-loader-facade';
|
||||||
|
|
||||||
const PROFILE_FLAGS_WITH_VALUE = new Set(['-p', '--prompt', '--effort']);
|
const PROFILE_FLAGS_WITH_VALUE = new Set(['-p', '--prompt', '--effort']);
|
||||||
|
const PROMPT_FLAGS_WITH_VALUE = new Set(['-p', '--prompt']);
|
||||||
|
const NUMERIC_CCS_FLAGS_WITH_VALUE = new Set(['--timeout', '--max-turns']);
|
||||||
|
const DASH_REJECTING_CCS_FLAGS_WITH_VALUE = new Set([
|
||||||
|
'--permission-mode',
|
||||||
|
'--fallback-model',
|
||||||
|
'--agents',
|
||||||
|
'--betas',
|
||||||
|
]);
|
||||||
|
const CCS_FLAGS_WITH_VALUE = new Set([
|
||||||
|
'-p',
|
||||||
|
'--prompt',
|
||||||
|
'--timeout',
|
||||||
|
'--permission-mode',
|
||||||
|
'--max-turns',
|
||||||
|
'--fallback-model',
|
||||||
|
'--agents',
|
||||||
|
'--betas',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isFlag(arg: string): boolean {
|
||||||
|
return arg.startsWith('-');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInlineCcsValueFlag(arg: string): boolean {
|
||||||
|
return Array.from(CCS_FLAGS_WITH_VALUE).some(
|
||||||
|
(flag) => flag.startsWith('--') && arg.startsWith(`${flag}=`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSkipCcsFlagValue(args: string[], index: number): boolean {
|
||||||
|
const arg = args[index];
|
||||||
|
if (index >= args.length - 1) return false;
|
||||||
|
if (PROMPT_FLAGS_WITH_VALUE.has(arg)) return true;
|
||||||
|
const nextArg = args[index + 1];
|
||||||
|
if (NUMERIC_CCS_FLAGS_WITH_VALUE.has(arg) && /^-\d/.test(nextArg)) return true;
|
||||||
|
if (
|
||||||
|
DASH_REJECTING_CCS_FLAGS_WITH_VALUE.has(arg) &&
|
||||||
|
isFlag(nextArg) &&
|
||||||
|
!nextArg.startsWith('--') &&
|
||||||
|
!CCS_FLAGS_WITH_VALUE.has(nextArg)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !isFlag(nextArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findInlineFlagValue(args: string[], flagName: string): string | undefined {
|
||||||
|
const prefix = `${flagName}=`;
|
||||||
|
const inlineArg = args.find((arg) => arg.startsWith(prefix));
|
||||||
|
if (inlineArg === undefined) return undefined;
|
||||||
|
const value = inlineArg.slice(prefix.length);
|
||||||
|
return value.trim().length > 0 ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse and validate a string flag value
|
* Parse and validate a string flag value
|
||||||
@@ -20,9 +73,15 @@ function parseStringFlag(
|
|||||||
options?: { allowDashPrefix?: boolean }
|
options?: { allowDashPrefix?: boolean }
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const index = args.indexOf(flagName);
|
const index = args.indexOf(flagName);
|
||||||
if (index === -1 || index >= args.length - 1) return undefined;
|
let value: string | undefined;
|
||||||
|
|
||||||
const value = args[index + 1];
|
if (index === -1) {
|
||||||
|
value = findInlineFlagValue(args, flagName);
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
} else {
|
||||||
|
if (index >= args.length - 1) return undefined;
|
||||||
|
value = args[index + 1];
|
||||||
|
}
|
||||||
|
|
||||||
// Reject dash-prefixed values (likely another flag)
|
// Reject dash-prefixed values (likely another flag)
|
||||||
if (!options?.allowDashPrefix && value.startsWith('-')) {
|
if (!options?.allowDashPrefix && value.startsWith('-')) {
|
||||||
@@ -165,9 +224,10 @@ export class DelegationHandler {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args[i].startsWith('--prompt=')) continue;
|
||||||
if (args[i].startsWith('--effort=')) continue;
|
if (args[i].startsWith('--effort=')) continue;
|
||||||
|
|
||||||
if (!args[i].startsWith('-')) {
|
if (!isFlag(args[i])) {
|
||||||
return args[i];
|
return args[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,6 +247,14 @@ export class DelegationHandler {
|
|||||||
|
|
||||||
const index = pIndex !== -1 ? pIndex : promptIndex;
|
const index = pIndex !== -1 ? pIndex : promptIndex;
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
const inlinePrompt = args.find((arg) => arg.startsWith('--prompt='));
|
||||||
|
if (inlinePrompt) {
|
||||||
|
const prompt = inlinePrompt.slice('--prompt='.length);
|
||||||
|
if (prompt.length > 0) return prompt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (index === -1 || index === args.length - 1) {
|
if (index === -1 || index === args.length - 1) {
|
||||||
console.error(fail('Missing prompt after -p flag'));
|
console.error(fail('Missing prompt after -p flag'));
|
||||||
console.error(' Usage: ccs glm -p "task description"');
|
console.error(' Usage: ccs glm -p "task description"');
|
||||||
@@ -215,15 +283,20 @@ export class DelegationHandler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Parse permission-mode (CLI flag overrides settings file)
|
// Parse permission-mode (CLI flag overrides settings file)
|
||||||
const permModeIndex = args.indexOf('--permission-mode');
|
const permissionMode = parseStringFlag(args, '--permission-mode');
|
||||||
if (permModeIndex !== -1 && permModeIndex < args.length - 1) {
|
if (permissionMode) {
|
||||||
options.permissionMode = args[permModeIndex + 1];
|
options.permissionMode = permissionMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse timeout (validated: positive integer, max 10 minutes)
|
// Parse timeout (validated: positive integer, max 10 minutes)
|
||||||
const timeoutIndex = args.indexOf('--timeout');
|
const timeoutIndex = args.indexOf('--timeout');
|
||||||
if (timeoutIndex !== -1 && timeoutIndex < args.length - 1) {
|
const inlineTimeout = findInlineFlagValue(args, '--timeout');
|
||||||
const rawVal = args[timeoutIndex + 1];
|
const rawTimeout =
|
||||||
|
timeoutIndex !== -1 && timeoutIndex < args.length - 1
|
||||||
|
? args[timeoutIndex + 1]
|
||||||
|
: inlineTimeout;
|
||||||
|
if (rawTimeout !== undefined) {
|
||||||
|
const rawVal = rawTimeout;
|
||||||
const val = parseInt(rawVal, 10);
|
const val = parseInt(rawVal, 10);
|
||||||
if (!isNaN(val) && val > 0 && val <= 600000) {
|
if (!isNaN(val) && val > 0 && val <= 600000) {
|
||||||
options.timeout = val;
|
options.timeout = val;
|
||||||
@@ -238,8 +311,13 @@ export class DelegationHandler {
|
|||||||
|
|
||||||
// Parse --max-turns (limit agentic turns, max 100)
|
// Parse --max-turns (limit agentic turns, max 100)
|
||||||
const maxTurnsIndex = args.indexOf('--max-turns');
|
const maxTurnsIndex = args.indexOf('--max-turns');
|
||||||
if (maxTurnsIndex !== -1 && maxTurnsIndex < args.length - 1) {
|
const inlineMaxTurns = findInlineFlagValue(args, '--max-turns');
|
||||||
const rawVal = args[maxTurnsIndex + 1];
|
const rawMaxTurns =
|
||||||
|
maxTurnsIndex !== -1 && maxTurnsIndex < args.length - 1
|
||||||
|
? args[maxTurnsIndex + 1]
|
||||||
|
: inlineMaxTurns;
|
||||||
|
if (rawMaxTurns !== undefined) {
|
||||||
|
const rawVal = rawMaxTurns;
|
||||||
const val = parseInt(rawVal, 10);
|
const val = parseInt(rawVal, 10);
|
||||||
if (!isNaN(val) && val > 0 && val <= 100) {
|
if (!isNaN(val) && val > 0 && val <= 100) {
|
||||||
options.maxTurns = val;
|
options.maxTurns = val;
|
||||||
@@ -271,42 +349,42 @@ export class DelegationHandler {
|
|||||||
// Parse --betas (experimental features)
|
// Parse --betas (experimental features)
|
||||||
options.betas = parseStringFlag(args, '--betas');
|
options.betas = parseStringFlag(args, '--betas');
|
||||||
|
|
||||||
// Collect extra args to pass through to Claude CLI
|
// Collect extra args to pass through to Claude CLI.
|
||||||
// CCS-handled flags with values (skip these and their values):
|
// Only CCS-owned flags are consumed here. Unknown/native Claude flags are preserved
|
||||||
const ccsFlagsWithValue = new Set([
|
// generically, including future variadic flags such as "--flag value1 value2".
|
||||||
'-p',
|
|
||||||
'--prompt',
|
|
||||||
'--timeout',
|
|
||||||
'--permission-mode',
|
|
||||||
'--max-turns',
|
|
||||||
'--fallback-model',
|
|
||||||
'--agents',
|
|
||||||
'--betas',
|
|
||||||
]);
|
|
||||||
const extraArgs: string[] = [];
|
const extraArgs: string[] = [];
|
||||||
const profile = this._extractProfile(args);
|
const profile = this._extractProfile(args);
|
||||||
|
let profileSkipped = false;
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
|
|
||||||
// Skip profile name (non-flag first arg)
|
// Skip only the first profile token. Later matching values may belong to native flags.
|
||||||
if (arg === profile && !arg.startsWith('-')) continue;
|
if (!profileSkipped && arg === profile && !isFlag(arg)) {
|
||||||
|
profileSkipped = true;
|
||||||
// Skip CCS-handled flags and their values
|
|
||||||
if (ccsFlagsWithValue.has(arg)) {
|
|
||||||
i++; // Skip next arg (the value)
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect flags and their values as passthrough
|
// Skip CCS-handled flags and their values.
|
||||||
if (arg.startsWith('-')) {
|
if (CCS_FLAGS_WITH_VALUE.has(arg)) {
|
||||||
extraArgs.push(arg);
|
if (shouldSkipCcsFlagValue(args, i)) i++;
|
||||||
// If next arg exists and doesn't start with '-', it's likely a value
|
continue;
|
||||||
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
||||||
extraArgs.push(args[i + 1]);
|
|
||||||
i++; // Skip the value we just added
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (isInlineCcsValueFlag(arg)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve native/future Claude flags and all adjacent values until the next flag.
|
||||||
|
if (isFlag(arg)) {
|
||||||
|
extraArgs.push(arg);
|
||||||
|
while (i + 1 < args.length && !isFlag(args[i + 1])) {
|
||||||
|
extraArgs.push(args[i + 1]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
extraArgs.push(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extraArgs.length > 0) {
|
if (extraArgs.length > 0) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ describe('DelegationHandler', () => {
|
|||||||
it('rejects negative timeout with warning', () => {
|
it('rejects negative timeout with warning', () => {
|
||||||
const options = handler._extractOptions(['glm', '-p', 'test', '--timeout', '-5000']);
|
const options = handler._extractOptions(['glm', '-p', 'test', '--timeout', '-5000']);
|
||||||
expect(options.timeout).toBeUndefined();
|
expect(options.timeout).toBeUndefined();
|
||||||
|
expect(options.extraArgs).toBeUndefined();
|
||||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,6 +56,22 @@ describe('DelegationHandler', () => {
|
|||||||
const options = handler._extractOptions(['glm', '-p', 'test', '--timeout']);
|
const options = handler._extractOptions(['glm', '-p', 'test', '--timeout']);
|
||||||
expect(options.timeout).toBeUndefined();
|
expect(options.timeout).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not drop following native flags when timeout value is missing', () => {
|
||||||
|
const options = handler._extractOptions([
|
||||||
|
'glm',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
'--timeout',
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
expect(options.timeout).toBeUndefined();
|
||||||
|
expect(options.extraArgs).toEqual([
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('_extractOptions - max-turns validation', () => {
|
describe('_extractOptions - max-turns validation', () => {
|
||||||
@@ -72,6 +89,7 @@ describe('DelegationHandler', () => {
|
|||||||
it('rejects negative max-turns with warning', () => {
|
it('rejects negative max-turns with warning', () => {
|
||||||
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns', '-5']);
|
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns', '-5']);
|
||||||
expect(options.maxTurns).toBeUndefined();
|
expect(options.maxTurns).toBeUndefined();
|
||||||
|
expect(options.extraArgs).toBeUndefined();
|
||||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,6 +109,18 @@ describe('DelegationHandler', () => {
|
|||||||
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns', '100']);
|
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns', '100']);
|
||||||
expect(options.maxTurns).toBe(100);
|
expect(options.maxTurns).toBe(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts inline max-turns syntax', () => {
|
||||||
|
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns=7']);
|
||||||
|
expect(options.maxTurns).toBe(7);
|
||||||
|
expect(options.extraArgs).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats empty inline max-turns as missing', () => {
|
||||||
|
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns=']);
|
||||||
|
expect(options.maxTurns).toBeUndefined();
|
||||||
|
expect(options.extraArgs).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('_extractOptions - fallback-model validation', () => {
|
describe('_extractOptions - fallback-model validation', () => {
|
||||||
@@ -99,6 +129,18 @@ describe('DelegationHandler', () => {
|
|||||||
expect(options.fallbackModel).toBe('sonnet');
|
expect(options.fallbackModel).toBe('sonnet');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts inline fallback-model syntax', () => {
|
||||||
|
const options = handler._extractOptions(['glm', '-p', 'test', '--fallback-model=sonnet']);
|
||||||
|
expect(options.fallbackModel).toBe('sonnet');
|
||||||
|
expect(options.extraArgs).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats empty inline fallback-model as missing', () => {
|
||||||
|
const options = handler._extractOptions(['glm', '-p', 'test', '--fallback-model=']);
|
||||||
|
expect(options.fallbackModel).toBeUndefined();
|
||||||
|
expect(options.extraArgs).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects dash-prefixed value with warning', () => {
|
it('rejects dash-prefixed value with warning', () => {
|
||||||
const options = handler._extractOptions([
|
const options = handler._extractOptions([
|
||||||
'glm',
|
'glm',
|
||||||
@@ -111,6 +153,24 @@ describe('DelegationHandler', () => {
|
|||||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not forward rejected dash-prefixed fallback-model values', () => {
|
||||||
|
const options = handler._extractOptions([
|
||||||
|
'glm',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
'--fallback-model',
|
||||||
|
'-sonnet',
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
expect(options.fallbackModel).toBeUndefined();
|
||||||
|
expect(options.extraArgs).toEqual([
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects empty string value', () => {
|
it('rejects empty string value', () => {
|
||||||
const options = handler._extractOptions(['glm', '-p', 'test', '--fallback-model', '']);
|
const options = handler._extractOptions(['glm', '-p', 'test', '--fallback-model', '']);
|
||||||
expect(options.fallbackModel).toBeUndefined();
|
expect(options.fallbackModel).toBeUndefined();
|
||||||
@@ -122,6 +182,54 @@ describe('DelegationHandler', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('_extractOptions - permission-mode parsing', () => {
|
||||||
|
it('accepts inline permission-mode syntax', () => {
|
||||||
|
const options = handler._extractOptions(['glm', '-p', 'test', '--permission-mode=plan']);
|
||||||
|
expect(options.permissionMode).toBe('plan');
|
||||||
|
expect(options.extraArgs).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats empty inline permission-mode as missing', () => {
|
||||||
|
const options = handler._extractOptions(['glm', '-p', 'test', '--permission-mode=']);
|
||||||
|
expect(options.permissionMode).toBe('acceptEdits');
|
||||||
|
expect(options.extraArgs).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not drop following native flags when permission-mode value is missing', () => {
|
||||||
|
const options = handler._extractOptions([
|
||||||
|
'glm',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
'--permission-mode',
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
expect(options.permissionMode).not.toBe('--channels');
|
||||||
|
expect(options.extraArgs).toEqual([
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not forward rejected dash-prefixed permission-mode values', () => {
|
||||||
|
const options = handler._extractOptions([
|
||||||
|
'glm',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
'--permission-mode',
|
||||||
|
'-invalid',
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
expect(options.permissionMode).toBe('acceptEdits');
|
||||||
|
expect(options.extraArgs).toEqual([
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('_extractOptions - agents JSON validation', () => {
|
describe('_extractOptions - agents JSON validation', () => {
|
||||||
it('accepts valid JSON for agents', () => {
|
it('accepts valid JSON for agents', () => {
|
||||||
const options = handler._extractOptions(['glm', '-p', 'test', '--agents', '{"name":"test"}']);
|
const options = handler._extractOptions(['glm', '-p', 'test', '--agents', '{"name":"test"}']);
|
||||||
@@ -189,6 +297,67 @@ describe('DelegationHandler', () => {
|
|||||||
const options = handler._extractOptions(['glm', '--effort', 'low', '-p', 'test']);
|
const options = handler._extractOptions(['glm', '--effort', 'low', '-p', 'test']);
|
||||||
expect(options.extraArgs).toEqual(['--effort', 'low']);
|
expect(options.extraArgs).toEqual(['--effort', 'low']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes Claude channels through to extraArgs', () => {
|
||||||
|
const options = handler._extractOptions([
|
||||||
|
'glm',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
expect(options.extraArgs).toEqual([
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves variadic native flag values without a Claude flag allowlist', () => {
|
||||||
|
const options = handler._extractOptions([
|
||||||
|
'glm',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
'--future-native',
|
||||||
|
'alpha',
|
||||||
|
'beta',
|
||||||
|
'--another-native',
|
||||||
|
'gamma',
|
||||||
|
]);
|
||||||
|
expect(options.extraArgs).toEqual([
|
||||||
|
'--future-native',
|
||||||
|
'alpha',
|
||||||
|
'beta',
|
||||||
|
'--another-native',
|
||||||
|
'gamma',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps native flag values that match the profile name', () => {
|
||||||
|
const options = handler._extractOptions(['glm', '-p', 'test', '--channels', 'glm']);
|
||||||
|
expect(options.extraArgs).toEqual(['--channels', 'glm']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not drop following native flags when a CCS flag value is missing', () => {
|
||||||
|
const options = handler._extractOptions([
|
||||||
|
'glm',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
'--fallback-model',
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
expect(options.fallbackModel).toBeUndefined();
|
||||||
|
expect(options.extraArgs).toEqual([
|
||||||
|
'--channels',
|
||||||
|
'plugin:telegram@claude-plugins-official',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps later CCS prompt flags from leaking when a prior CCS value is invalid', () => {
|
||||||
|
const options = handler._extractOptions(['glm', '--fallback-model', '-p', 'test']);
|
||||||
|
expect(options.fallbackModel).toBeUndefined();
|
||||||
|
expect(options.extraArgs).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('_extractProfile', () => {
|
describe('_extractProfile', () => {
|
||||||
@@ -202,6 +371,11 @@ describe('DelegationHandler', () => {
|
|||||||
expect(profile).toBe('');
|
expect(profile).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('skips inline prompt before profile names', () => {
|
||||||
|
const profile = handler._extractProfile(['--prompt=test', 'glm']);
|
||||||
|
expect(profile).toBe('glm');
|
||||||
|
});
|
||||||
|
|
||||||
it('skips flag values correctly', () => {
|
it('skips flag values correctly', () => {
|
||||||
const profile = handler._extractProfile(['-p', 'test', 'kimi']);
|
const profile = handler._extractProfile(['-p', 'test', 'kimi']);
|
||||||
expect(profile).toBe('kimi');
|
expect(profile).toBe('kimi');
|
||||||
@@ -217,4 +391,24 @@ describe('DelegationHandler', () => {
|
|||||||
expect(profile).toBe('glm');
|
expect(profile).toBe('glm');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('_extractPrompt', () => {
|
||||||
|
it('accepts inline prompt syntax', () => {
|
||||||
|
const prompt = handler._extractPrompt(['glm', '--prompt=test']);
|
||||||
|
expect(prompt).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects empty inline prompt syntax', () => {
|
||||||
|
const exitSpy = spyOn(process, 'exit').mockImplementation(((code?: number) => {
|
||||||
|
throw new Error(`process.exit(${code})`);
|
||||||
|
}) as typeof process.exit);
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(() => handler._extractPrompt(['glm', '--prompt='])).toThrow('process.exit(1)');
|
||||||
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||||
|
} finally {
|
||||||
|
exitSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user