mcoda 0.1.72 → 0.1.74

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.
@@ -1 +1 @@
1
- {"version":3,"file":"AgentsCommands.d.ts","sourceRoot":"","sources":["../../../src/commands/agents/AgentsCommands.ts"],"names":[],"mappings":"AAwXA,qBAAa,cAAc;WACZ,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAsWhD"}
1
+ {"version":3,"file":"AgentsCommands.d.ts","sourceRoot":"","sources":["../../../src/commands/agents/AgentsCommands.ts"],"names":[],"mappings":"AA+lBA,qBAAa,cAAc;WACZ,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CA8YhD"}
@@ -1,4 +1,5 @@
1
1
  import { AgentsApi, WorkspaceResolver } from "@mcoda/core";
2
+ import { defaultLocalRunnerKindForAdapter, isLocalOpenAiCompatibleAdapter, isReservedLocalRunnerExtraBodyKey, isSecretLocalRunnerHeaderKey, normalizeLocalRunnerAuthMode, normalizeLocalRunnerKind, normalizeLocalRunnerResponseFormatStrategy, } from "@mcoda/shared";
2
3
  import readline from "node:readline";
3
4
  const parseArgs = (argv) => {
4
5
  const flags = {};
@@ -49,7 +50,7 @@ Subcommands:
49
50
  limits Show tracked usage-limit windows/reset times
50
51
  --agent <NAME> Filter by agent slug/id
51
52
  add <NAME> Create a global agent
52
- --adapter <TYPE> Adapter slug (openai-api|zhipu-api|codex-cli|claude-cli|gemini-cli|local-model|qa-cli|ollama-remote)
53
+ --adapter <TYPE> Adapter slug (openai-api|zhipu-api|codex-cli|claude-cli|gemini-cli|local-model|qa-cli|ollama-remote|openai-compatible-local|vllm-local|llama-cpp-local|llamacpp-local)
53
54
  --model <MODEL> Default model name
54
55
  --rating <N> Relative capability rating (higher is stronger)
55
56
  --reasoning-rating <N> Relative reasoning strength rating (higher is stronger)
@@ -64,14 +65,42 @@ Subcommands:
64
65
  --job-path <PATH> Optional job prompt path
65
66
  --character-path <PATH> Optional character prompt path
66
67
  --config-base-url <URL> Base URL for remote adapters (e.g., http://host:11434 for ollama-remote)
68
+ --config-runner-kind <K> Local runner kind (vllm|llama-cpp|llama-cpp-python|lm-studio|localai|sglang|tgi|custom)
69
+ --config-auth-mode <M> Local auth mode (none|bearer|dummy-bearer)
70
+ --config-dummy-bearer-token <T> Non-secret dummy bearer token for local runners
71
+ --config-header <K=V> Repeatable non-secret local runner header
72
+ --config-extra-body-json <JSON> Extra OpenAI-compatible request body object
73
+ --config-response-format-strategy <S> Local response format strategy
74
+ --config-health-path <P> Local runner health path override
75
+ --config-models-path <P> Local runner models path override
67
76
  --config-temperature <N> Temperature override for supported adapters
68
77
  --config-thinking <BOOL> Enable thinking mode for supported adapters
69
78
  update <NAME> Update adapter/model/capabilities/prompts for an agent
79
+ --adapter <TYPE> Adapter slug (supports local OpenAI-compatible aliases)
80
+ --model <MODEL> Default model name
81
+ --rating <N> Relative capability rating (higher is stronger)
82
+ --reasoning-rating <N> Relative reasoning strength rating (higher is stronger)
70
83
  --max-complexity <N> Max task complexity the agent should handle (1-10)
71
84
  --openai-compatible <B> OpenAI-compatible API support (true/false)
72
85
  --context-window <N> Context window size (tokens)
73
86
  --max-output-tokens <N> Max output tokens per response
74
87
  --supports-tools <B> Tool-calling support (true/false)
88
+ --best-usage <TEXT> Primary usage area (e.g., code_write, ui_ux_docs)
89
+ --cost-per-million <N> Cost per 1M tokens (0 for local models)
90
+ --capability <CAP> Repeatable capabilities to attach
91
+ --job-path <PATH> Optional job prompt path
92
+ --character-path <PATH> Optional character prompt path
93
+ --config-base-url <URL> Base URL for remote/local runner adapters
94
+ --config-runner-kind <K> Local runner kind (vllm|llama-cpp|llama-cpp-python|lm-studio|localai|sglang|tgi|custom)
95
+ --config-auth-mode <M> Local auth mode (none|bearer|dummy-bearer)
96
+ --config-dummy-bearer-token <T> Non-secret dummy bearer token for local runners
97
+ --config-header <K=V> Repeatable non-secret local runner header
98
+ --config-extra-body-json <JSON> Extra OpenAI-compatible request body object
99
+ --config-response-format-strategy <S> Local response format strategy
100
+ --config-health-path <P> Local runner health path override
101
+ --config-models-path <P> Local runner models path override
102
+ --config-temperature <N> Temperature override for supported adapters
103
+ --config-thinking <BOOL> Enable thinking mode for supported adapters
75
104
  delete|remove <NAME> Remove an agent (use --force to ignore routing/default references)
76
105
  --force Force deletion even if referenced
77
106
  auth set <NAME> Store credentials (use --api-key or interactive prompt)
@@ -161,6 +190,74 @@ const parseBooleanFlag = (value, label) => {
161
190
  return false;
162
191
  throw new Error(`Invalid ${label}; expected true/false`);
163
192
  };
193
+ const CONFIG_FLAG_NAMES = [
194
+ "config-base-url",
195
+ "config-runner-kind",
196
+ "config-auth-mode",
197
+ "config-dummy-bearer-token",
198
+ "config-header",
199
+ "config-extra-body-json",
200
+ "config-response-format-strategy",
201
+ "config-health-path",
202
+ "config-models-path",
203
+ "config-temperature",
204
+ "config-thinking",
205
+ ];
206
+ const hasConfigFlags = (flags) => CONFIG_FLAG_NAMES.some((flagName) => flags[flagName] !== undefined);
207
+ const getLastStringFlag = (value, label) => {
208
+ if (value === undefined)
209
+ return undefined;
210
+ const raw = Array.isArray(value) ? value[value.length - 1] : value;
211
+ if (typeof raw === "boolean") {
212
+ throw new Error(`Invalid ${label}; expected a value`);
213
+ }
214
+ const trimmed = String(raw).trim();
215
+ if (!trimmed) {
216
+ throw new Error(`Invalid ${label}; expected a non-empty value`);
217
+ }
218
+ return trimmed;
219
+ };
220
+ const getStringFlagValues = (value, label) => {
221
+ if (value === undefined)
222
+ return [];
223
+ const values = Array.isArray(value) ? value : [value];
224
+ return values.map((raw) => {
225
+ if (typeof raw === "boolean") {
226
+ throw new Error(`Invalid ${label}; expected a value`);
227
+ }
228
+ const trimmed = String(raw).trim();
229
+ if (!trimmed) {
230
+ throw new Error(`Invalid ${label}; expected a non-empty value`);
231
+ }
232
+ return trimmed;
233
+ });
234
+ };
235
+ const validateHttpUrl = (value, label) => {
236
+ let parsed;
237
+ try {
238
+ parsed = new URL(value);
239
+ }
240
+ catch {
241
+ throw new Error(`Invalid ${label}; expected an absolute http(s) URL`);
242
+ }
243
+ if (!["http:", "https:"].includes(parsed.protocol) || !parsed.hostname) {
244
+ throw new Error(`Invalid ${label}; expected an absolute http(s) URL`);
245
+ }
246
+ return value;
247
+ };
248
+ const isLoopbackBaseUrl = (value) => {
249
+ try {
250
+ const hostname = new URL(value).hostname.toLowerCase().replace(/^\[|\]$/g, "");
251
+ if (hostname === "localhost" || hostname === "::1")
252
+ return true;
253
+ if (/^127(?:\.\d{1,3}){3}$/.test(hostname))
254
+ return true;
255
+ return false;
256
+ }
257
+ catch {
258
+ return false;
259
+ }
260
+ };
164
261
  const resolveListRefreshHealth = (flags) => {
165
262
  const refreshHealth = parseBooleanFlag(flags["refresh-health"], "--refresh-health");
166
263
  const noRefreshHealth = parseBooleanFlag(flags["no-refresh-health"], "--no-refresh-health");
@@ -185,10 +282,98 @@ const parseCostPerMillion = (value) => {
185
282
  }
186
283
  return parsed;
187
284
  };
188
- const parseConfig = (flags) => {
285
+ const parseConfigExtraBody = (raw) => {
286
+ let parsed;
287
+ try {
288
+ parsed = JSON.parse(raw);
289
+ }
290
+ catch {
291
+ throw new Error("Invalid --config-extra-body-json; expected a JSON object");
292
+ }
293
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
294
+ throw new Error("Invalid --config-extra-body-json; expected a JSON object");
295
+ }
296
+ for (const key of Object.keys(parsed)) {
297
+ if (isReservedLocalRunnerExtraBodyKey(key)) {
298
+ throw new Error(`Invalid --config-extra-body-json; reserved OpenAI request key "${key}" is not allowed`);
299
+ }
300
+ }
301
+ return parsed;
302
+ };
303
+ const parseConfigHeaders = (value) => {
304
+ const entries = getStringFlagValues(value, "--config-header");
305
+ if (entries.length === 0)
306
+ return undefined;
307
+ const headers = {};
308
+ for (const entry of entries) {
309
+ const separatorIndex = entry.indexOf("=");
310
+ if (separatorIndex <= 0) {
311
+ throw new Error("Invalid --config-header; expected key=value");
312
+ }
313
+ const key = entry.slice(0, separatorIndex).trim();
314
+ const headerValue = entry.slice(separatorIndex + 1).trim();
315
+ if (!key || !headerValue) {
316
+ throw new Error("Invalid --config-header; expected non-empty key=value");
317
+ }
318
+ if (isSecretLocalRunnerHeaderKey(key)) {
319
+ throw new Error(`Invalid --config-header; secret-bearing header "${key}" must be stored as auth, not config`);
320
+ }
321
+ headers[key] = headerValue;
322
+ }
323
+ return Object.keys(headers).length ? headers : undefined;
324
+ };
325
+ const parseConfig = (flags, options = {}) => {
189
326
  const config = {};
190
- if (flags["config-base-url"])
191
- config.baseUrl = String(flags["config-base-url"]);
327
+ const localOpenAiCompatible = isLocalOpenAiCompatibleAdapter(options.adapter);
328
+ const applyLocalDefaults = options.applyLocalDefaults ?? true;
329
+ const baseUrl = getLastStringFlag(flags["config-base-url"], "--config-base-url");
330
+ if (baseUrl)
331
+ config.baseUrl = validateHttpUrl(baseUrl, "--config-base-url");
332
+ const runnerKindValue = getLastStringFlag(flags["config-runner-kind"], "--config-runner-kind");
333
+ if (runnerKindValue) {
334
+ const runnerKind = normalizeLocalRunnerKind(runnerKindValue);
335
+ if (!runnerKind)
336
+ throw new Error(`Invalid --config-runner-kind; unknown local runner kind "${runnerKindValue}"`);
337
+ config.runnerKind = runnerKind;
338
+ }
339
+ else if (applyLocalDefaults) {
340
+ const defaultRunnerKind = defaultLocalRunnerKindForAdapter(options.adapter);
341
+ if (defaultRunnerKind)
342
+ config.runnerKind = defaultRunnerKind;
343
+ }
344
+ const authModeValue = getLastStringFlag(flags["config-auth-mode"], "--config-auth-mode");
345
+ if (authModeValue) {
346
+ const authMode = normalizeLocalRunnerAuthMode(authModeValue);
347
+ if (!authMode)
348
+ throw new Error(`Invalid --config-auth-mode; unknown local auth mode "${authModeValue}"`);
349
+ config.authMode = authMode;
350
+ }
351
+ else if (localOpenAiCompatible && applyLocalDefaults) {
352
+ config.authMode = "none";
353
+ }
354
+ const dummyBearerToken = getLastStringFlag(flags["config-dummy-bearer-token"], "--config-dummy-bearer-token");
355
+ if (dummyBearerToken)
356
+ config.dummyBearerToken = dummyBearerToken;
357
+ const headers = parseConfigHeaders(flags["config-header"]);
358
+ if (headers)
359
+ config.headers = headers;
360
+ const extraBodyRaw = getLastStringFlag(flags["config-extra-body-json"], "--config-extra-body-json");
361
+ if (extraBodyRaw)
362
+ config.extraBody = parseConfigExtraBody(extraBodyRaw);
363
+ const responseFormatStrategyValue = getLastStringFlag(flags["config-response-format-strategy"], "--config-response-format-strategy");
364
+ if (responseFormatStrategyValue) {
365
+ const responseFormatStrategy = normalizeLocalRunnerResponseFormatStrategy(responseFormatStrategyValue);
366
+ if (!responseFormatStrategy) {
367
+ throw new Error(`Invalid --config-response-format-strategy; unknown response format strategy "${responseFormatStrategyValue}"`);
368
+ }
369
+ config.responseFormatStrategy = responseFormatStrategy;
370
+ }
371
+ const healthPath = getLastStringFlag(flags["config-health-path"], "--config-health-path");
372
+ if (healthPath)
373
+ config.healthPath = healthPath;
374
+ const modelsPath = getLastStringFlag(flags["config-models-path"], "--config-models-path");
375
+ if (modelsPath)
376
+ config.modelsPath = modelsPath;
192
377
  if (flags["config-temperature"] !== undefined) {
193
378
  const raw = flags["config-temperature"];
194
379
  const parsed = typeof raw === "number" ? raw : Number.parseFloat(String(raw));
@@ -215,8 +400,20 @@ const parseConfig = (flags) => {
215
400
  }
216
401
  }
217
402
  }
403
+ if (options.requireBaseUrl && !config.baseUrl && !config.endpoint && !config.apiBaseUrl) {
404
+ throw new Error("CONFIG_REQUIRED: --config-base-url is required for local OpenAI-compatible adapters");
405
+ }
406
+ if (config.authMode === "none" && typeof config.baseUrl === "string" && !isLoopbackBaseUrl(config.baseUrl)) {
407
+ options.warn?.(`Warning: ${options.adapter ?? "local runner"} uses authMode=none with non-loopback baseUrl ${config.baseUrl}.`);
408
+ }
218
409
  return Object.keys(config).length ? config : undefined;
219
410
  };
411
+ const hasLocalRunnerBaseUrl = (config) => Boolean(config?.baseUrl || config?.endpoint || config?.apiBaseUrl);
412
+ const mergeConfigPatch = (existing, patch) => {
413
+ if (!patch)
414
+ return undefined;
415
+ return existing ? { ...existing, ...patch } : patch;
416
+ };
220
417
  const DEFAULT_OLLAMA_CAPABILITIES = ["plan", "code_write", "code_review"];
221
418
  const pad = (value, width) => value.padEnd(width, " ");
222
419
  const truncate = (value, max) => {
@@ -554,22 +751,33 @@ export class AgentsCommands {
554
751
  const name = parsed.positionals[0];
555
752
  if (!name)
556
753
  throw new Error("agent add requires a slug/name\n\n" + USAGE);
754
+ const adapter = String(parsed.flags.adapter ?? "openai-api");
755
+ const localOpenAiCompatible = isLocalOpenAiCompatibleAdapter(adapter);
557
756
  const capabilities = parseCapabilities(parsed.flags.capability) ??
558
- (String(parsed.flags.adapter ?? "openai-api") === "ollama-remote" ? DEFAULT_OLLAMA_CAPABILITIES : []);
757
+ (adapter === "ollama-remote" ? DEFAULT_OLLAMA_CAPABILITIES : []);
559
758
  const prompts = parsePrompts(parsed.flags);
560
- const config = parseConfig(parsed.flags);
759
+ const config = parseConfig(parsed.flags, {
760
+ adapter,
761
+ requireBaseUrl: localOpenAiCompatible,
762
+ warn: (message) => console.warn(message),
763
+ });
561
764
  const rating = parseRating(parsed.flags.rating);
562
765
  const reasoningRating = parseReasoningRating(parsed.flags["reasoning-rating"]);
563
766
  const maxComplexity = parseMaxComplexity(parsed.flags["max-complexity"]);
564
- const openaiCompatible = parseBooleanFlag(parsed.flags["openai-compatible"], "--openai-compatible");
767
+ const openaiCompatibleFlag = parseBooleanFlag(parsed.flags["openai-compatible"], "--openai-compatible");
768
+ if (localOpenAiCompatible && openaiCompatibleFlag === false) {
769
+ throw new Error("Local OpenAI-compatible adapters require --openai-compatible true");
770
+ }
771
+ const openaiCompatible = localOpenAiCompatible ? true : openaiCompatibleFlag;
565
772
  const contextWindow = parsePositiveInt(parsed.flags["context-window"], "--context-window");
566
773
  const maxOutputTokens = parsePositiveInt(parsed.flags["max-output-tokens"], "--max-output-tokens");
567
774
  const supportsTools = parseBooleanFlag(parsed.flags["supports-tools"], "--supports-tools");
568
775
  const bestUsage = parsed.flags["best-usage"] ? String(parsed.flags["best-usage"]) : undefined;
569
- const costPerMillion = parseCostPerMillion(parsed.flags["cost-per-million"]);
776
+ const parsedCostPerMillion = parseCostPerMillion(parsed.flags["cost-per-million"]);
777
+ const costPerMillion = localOpenAiCompatible && parsedCostPerMillion === undefined ? 0 : parsedCostPerMillion;
570
778
  const agent = await api.createAgent({
571
779
  slug: name,
572
- adapter: String(parsed.flags.adapter ?? "openai-api"),
780
+ adapter,
573
781
  defaultModel: parsed.flags.model ? String(parsed.flags.model) : undefined,
574
782
  rating,
575
783
  reasoningRating,
@@ -592,20 +800,45 @@ export class AgentsCommands {
592
800
  const name = parsed.positionals[0];
593
801
  if (!name)
594
802
  throw new Error("agent update requires a slug/name\n\n" + USAGE);
803
+ const existing = await api.getAgent(name);
804
+ const adapter = parsed.flags.adapter ? String(parsed.flags.adapter) : undefined;
805
+ const effectiveAdapter = adapter ?? existing.adapter;
806
+ const localOpenAiCompatible = isLocalOpenAiCompatibleAdapter(effectiveAdapter);
807
+ const switchingToLocalOpenAiCompatible = adapter !== undefined && isLocalOpenAiCompatibleAdapter(adapter);
595
808
  const capabilities = parseCapabilities(parsed.flags.capability);
596
809
  const prompts = parsePrompts(parsed.flags);
597
- const config = parseConfig(parsed.flags);
810
+ const configPatch = hasConfigFlags(parsed.flags) || switchingToLocalOpenAiCompatible
811
+ ? parseConfig(parsed.flags, {
812
+ adapter: effectiveAdapter,
813
+ applyLocalDefaults: switchingToLocalOpenAiCompatible,
814
+ })
815
+ : undefined;
816
+ const existingConfig = existing.config && typeof existing.config === "object"
817
+ ? existing.config
818
+ : undefined;
819
+ const config = mergeConfigPatch(existingConfig, configPatch);
820
+ if (config && localOpenAiCompatible && !hasLocalRunnerBaseUrl(config)) {
821
+ throw new Error("CONFIG_REQUIRED: --config-base-url is required for local OpenAI-compatible adapters");
822
+ }
823
+ if (config?.authMode === "none" && typeof config.baseUrl === "string" && !isLoopbackBaseUrl(config.baseUrl)) {
824
+ console.warn(`Warning: ${effectiveAdapter ?? "local runner"} uses authMode=none with non-loopback baseUrl ${config.baseUrl}.`);
825
+ }
598
826
  const rating = parseRating(parsed.flags.rating);
599
827
  const reasoningRating = parseReasoningRating(parsed.flags["reasoning-rating"]);
600
828
  const maxComplexity = parseMaxComplexity(parsed.flags["max-complexity"]);
601
- const openaiCompatible = parseBooleanFlag(parsed.flags["openai-compatible"], "--openai-compatible");
829
+ const openaiCompatibleFlag = parseBooleanFlag(parsed.flags["openai-compatible"], "--openai-compatible");
830
+ if (localOpenAiCompatible && openaiCompatibleFlag === false) {
831
+ throw new Error("Local OpenAI-compatible adapters require --openai-compatible true");
832
+ }
833
+ const openaiCompatible = localOpenAiCompatible ? true : openaiCompatibleFlag;
602
834
  const contextWindow = parsePositiveInt(parsed.flags["context-window"], "--context-window");
603
835
  const maxOutputTokens = parsePositiveInt(parsed.flags["max-output-tokens"], "--max-output-tokens");
604
836
  const supportsTools = parseBooleanFlag(parsed.flags["supports-tools"], "--supports-tools");
605
837
  const bestUsage = parsed.flags["best-usage"] ? String(parsed.flags["best-usage"]) : undefined;
606
- const costPerMillion = parseCostPerMillion(parsed.flags["cost-per-million"]);
838
+ const parsedCostPerMillion = parseCostPerMillion(parsed.flags["cost-per-million"]);
839
+ const costPerMillion = localOpenAiCompatible && parsedCostPerMillion === undefined ? 0 : parsedCostPerMillion;
607
840
  const agent = await api.updateAgent(name, {
608
- adapter: parsed.flags.adapter ? String(parsed.flags.adapter) : undefined,
841
+ adapter,
609
842
  defaultModel: parsed.flags.model ? String(parsed.flags.model) : undefined,
610
843
  rating,
611
844
  reasoningRating,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcoda",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "description": "Local-first CLI for planning, documentation, and execution workflows with agent assistance.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -47,12 +47,12 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "yaml": "^2.4.2",
50
- "@mcoda/core": "0.1.72",
51
- "@mcoda/shared": "0.1.72"
50
+ "@mcoda/core": "0.1.74",
51
+ "@mcoda/shared": "0.1.74"
52
52
  },
53
53
  "devDependencies": {
54
- "@mcoda/db": "0.1.72",
55
- "@mcoda/integrations": "0.1.72"
54
+ "@mcoda/db": "0.1.74",
55
+ "@mcoda/integrations": "0.1.74"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsc -p tsconfig.json",