demian-cli 1.1.0 → 1.1.2

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/README.md CHANGED
@@ -28,6 +28,8 @@ Additional locale: [한국어](./docs/ko/README.md)
28
28
  grants.
29
29
  - Project and user configuration through `.demian/config.json` or
30
30
  `.demian/config.jsond`.
31
+ - First-run setup plus terminal config screens and `demian config` commands for
32
+ provider defaults, connection fields, and model profiles.
31
33
 
32
34
  ## Requirements
33
35
 
@@ -68,9 +70,12 @@ npx demian
68
70
 
69
71
  In the TUI:
70
72
 
71
- - Press `p` to choose a provider.
72
- - Press `m` to edit the model.
73
- - Press `a` to choose the main agent.
73
+ - Press `Esc` then `k` to open the command palette.
74
+ - Press `Esc` then `s` to select a saved session.
75
+ - Press `Esc` then `p` to choose a provider.
76
+ - Press `Esc` then `m` to edit the model.
77
+ - Press `Esc` then `a` to choose the main agent.
78
+ - Press `Esc` then `c` to open the config screen for persistent provider/model setup.
74
79
  - Type your request and press `Enter`.
75
80
 
76
81
  3. Try a first prompt:
@@ -105,6 +110,12 @@ Demian loads configuration from user and project locations:
105
110
 
106
111
  `.jsond` files support comments and trailing commas.
107
112
 
113
+ The terminal UI creates `~/.demian/config.json` automatically when no user
114
+ config exists. Press `Esc` then `c` to open the config screen. From there you
115
+ can set the default provider, choose or add a default model profile for a
116
+ provider, edit the selected provider's `baseURL`, `apiKeyEnv`, and auth header,
117
+ and add common provider presets without hand-editing JSON.
118
+
108
119
  ### OpenAI-Compatible Example
109
120
 
110
121
  Create `~/.demian/config.jsond`:
@@ -272,12 +283,42 @@ demian-plain "Give me a release checklist"
272
283
 
273
284
  Inside the TUI:
274
285
 
286
+ - `Esc` then `k`: open the command palette.
287
+ - `Esc` then `s`: select a saved session.
288
+ - `Esc` then `p`: choose a provider.
289
+ - `Esc` then `m`: edit the model.
290
+ - `Esc` then `a`: choose the main agent.
291
+ - `Esc` then `o`: choose the default permission preset.
292
+ - `Esc` then `c`: configure provider defaults and model profiles.
293
+ - `Esc` then `u`: clear the composer.
294
+ - `Esc` then `q`: quit.
275
295
  - `/compact`: summarize older conversation history.
296
+ - `/session list`: show saved CLI conversations.
297
+ - `/session new [title]`: start a fresh conversation.
298
+ - `/session switch <number|id|title>`: switch to a saved conversation.
299
+ - `/session rename <title>` and `/session delete <number|id|title>`: manage conversations.
276
300
  - `/stop`: stop active work.
277
301
  - `/exit` or `/quit`: close Demian.
278
302
  - `/goal ...`: start a verified long-running objective.
279
303
  - `/cowork ...`: explicitly coordinate cowork sub agents.
280
304
 
305
+ Config and auth helpers:
306
+
307
+ ```sh
308
+ demian config setup
309
+ demian config init [--path ~/.demian/config.json] [--provider openai]
310
+ demian config list
311
+ demian config models <provider> [--refresh]
312
+ demian config add-provider <preset|openai-compatible> [--name <name>]
313
+ demian config add-model <provider> --name <name> --model <model>
314
+ demian config set-default-model <provider> [--profile <name> | --model <model>]
315
+ demian config update-provider <provider> [--base-url <url>] [--api-key-env <env>]
316
+ demian config path
317
+ demian auth login codex
318
+ demian auth login claudecode
319
+ demian auth status
320
+ ```
321
+
281
322
  ## Tools and Permissions
282
323
 
283
324
  Demian can use local tools for code work:
@@ -338,6 +379,8 @@ Web search asks for permission because it may call paid third-party APIs.
338
379
  Demian uses both user-level and project-level storage:
339
380
 
340
381
  - `~/.demian/config.json` or `~/.demian/config.jsond`: user config.
382
+ - `~/.demian/conversations.json` and `~/.demian/conversations/`: saved CLI and
383
+ VS Code conversations.
341
384
  - `.demian/config.json` or `.demian/config.jsond`: project config.
342
385
  - `.demian/preferences.json`: remembered provider/model choice for the current
343
386
  workspace.
package/dist/cli.mjs CHANGED
@@ -24598,10 +24598,14 @@ async function addModelProfile(options) {
24598
24598
  const provider = objectValue(providers[options.provider]);
24599
24599
  const profiles = Array.isArray(provider.modelProfiles) ? [...provider.modelProfiles] : [];
24600
24600
  const displayName = options.displayName ?? options.name;
24601
+ const previousName = normalizeOptionalName(options.previousName);
24602
+ const existingByPreviousName = previousName ? profiles.findIndex((entry) => entry.name === previousName) : -1;
24601
24603
  const existingByName = profiles.findIndex((entry) => entry.name === options.name);
24602
24604
  const existingByDisplay = profiles.findIndex((entry) => entry.displayName === displayName);
24603
- if (existingByName >= 0 && !options.force) throw new Error(`Profile name "${options.name}" already exists on provider ${options.provider}. Use --force to overwrite it.`);
24604
- if (existingByDisplay >= 0 && existingByDisplay !== existingByName && !options.force) {
24605
+ const updateIndex = existingByPreviousName >= 0 ? existingByPreviousName : existingByName;
24606
+ if (existingByName >= 0 && existingByName !== updateIndex) throw new Error(`Profile name "${options.name}" already exists on provider ${options.provider}. Pick a different profile name.`);
24607
+ if (existingByName >= 0 && existingByPreviousName < 0 && !options.force) throw new Error(`Profile name "${options.name}" already exists on provider ${options.provider}. Use --force to overwrite it.`);
24608
+ if (existingByDisplay >= 0 && existingByDisplay !== updateIndex && !options.force) {
24605
24609
  throw new Error(`Profile displayName "${displayName}" already exists on provider ${options.provider} (profile name: ${profiles[existingByDisplay].name}). Use --force or pick a different --display-name.`);
24606
24610
  }
24607
24611
  const next = {
@@ -24612,7 +24616,7 @@ async function addModelProfile(options) {
24612
24616
  ...options.apiKey !== void 0 || options.apiKeyEnv ? { apiKey: options.apiKey ?? "" } : {},
24613
24617
  ...options.apiKeyEnv ? { apiKeyEnv: options.apiKeyEnv } : {}
24614
24618
  };
24615
- if (existingByName >= 0) profiles[existingByName] = next;
24619
+ if (updateIndex >= 0) profiles[updateIndex] = next;
24616
24620
  else profiles.push(next);
24617
24621
  provider.modelProfiles = profiles;
24618
24622
  providers[options.provider] = provider;
@@ -24622,6 +24626,70 @@ async function addModelProfile(options) {
24622
24626
  await writeJsonAtomic(filePath, content);
24623
24627
  return { path: filePath, created: true, content };
24624
24628
  }
24629
+ async function setDefaultModelProfile(options) {
24630
+ const filePath = expandHome2(options.path ?? defaultUserConfigPath());
24631
+ const config = await readConfigObject(filePath);
24632
+ const providers = objectValue(config.providers);
24633
+ const provider = providers[options.provider];
24634
+ if (!provider || typeof provider !== "object" || Array.isArray(provider)) throw new Error(`Provider ${options.provider} does not exist.`);
24635
+ const providerConfig = { ...provider };
24636
+ const profiles = Array.isArray(providerConfig.modelProfiles) ? [...providerConfig.modelProfiles] : [];
24637
+ const requestedName = normalizeOptionalName(options.profileName ?? options.name);
24638
+ const requestedModel = normalizeOptionalName(options.model);
24639
+ const requestedDisplayName = normalizeOptionalName(options.displayName) ?? requestedModel ?? requestedName;
24640
+ let index = profiles.findIndex((entry) => requestedName && entry.name === requestedName);
24641
+ if (index < 0 && requestedModel) index = profiles.findIndex((entry) => entry.model === requestedModel);
24642
+ if (index < 0 && requestedDisplayName) index = profiles.findIndex((entry) => entry.displayName === requestedDisplayName);
24643
+ const profile = index >= 0 ? {
24644
+ ...profiles[index],
24645
+ ...requestedName ? { name: requestedName } : {},
24646
+ ...requestedDisplayName ? { displayName: requestedDisplayName } : {},
24647
+ ...requestedModel ? { model: requestedModel } : {},
24648
+ ...options.baseURL ? { baseURL: options.baseURL } : {},
24649
+ ...options.apiKey !== void 0 || options.apiKeyEnv ? { apiKey: options.apiKey ?? "" } : {},
24650
+ ...options.apiKeyEnv ? { apiKeyEnv: options.apiKeyEnv } : {}
24651
+ } : newModelProfile({
24652
+ name: requestedName,
24653
+ displayName: requestedDisplayName,
24654
+ model: requestedModel ?? requestedName,
24655
+ baseURL: options.baseURL,
24656
+ apiKey: options.apiKey,
24657
+ apiKeyEnv: options.apiKeyEnv
24658
+ });
24659
+ if (!profile.name || typeof profile.name !== "string") throw new Error("Model profile name is required.");
24660
+ if (!profile.model || typeof profile.model !== "string") throw new Error("Model ID is required.");
24661
+ if (index >= 0) profiles.splice(index, 1);
24662
+ profiles.unshift(profile);
24663
+ providerConfig.modelProfiles = profiles;
24664
+ providers[options.provider] = providerConfig;
24665
+ config.providers = providers;
24666
+ const content = `${JSON.stringify(config, null, 2)}
24667
+ `;
24668
+ await writeJsonAtomic(filePath, content);
24669
+ return { path: filePath, created: true, content };
24670
+ }
24671
+ async function updateProviderSettings(options) {
24672
+ const filePath = expandHome2(options.path ?? defaultUserConfigPath());
24673
+ const config = await readConfigObject(filePath);
24674
+ const providers = objectValue(config.providers);
24675
+ const provider = providers[options.provider];
24676
+ if (!provider || typeof provider !== "object" || Array.isArray(provider)) throw new Error(`Provider ${options.provider} does not exist.`);
24677
+ const providerConfig = { ...provider };
24678
+ if (options.baseURL !== void 0) setOrDelete(providerConfig, "baseURL", options.baseURL);
24679
+ if (options.apiKey !== void 0) setOrDelete(providerConfig, "apiKey", options.apiKey);
24680
+ if (options.apiKeyEnv !== void 0) setOrDelete(providerConfig, "apiKeyEnv", options.apiKeyEnv);
24681
+ if (options.authHeader !== void 0) {
24682
+ const header = options.authHeader.trim();
24683
+ if (header) providerConfig.auth = { type: "api-key", header };
24684
+ else delete providerConfig.auth;
24685
+ }
24686
+ providers[options.provider] = providerConfig;
24687
+ config.providers = providers;
24688
+ const content = `${JSON.stringify(config, null, 2)}
24689
+ `;
24690
+ await writeJsonAtomic(filePath, content);
24691
+ return { path: filePath, created: true, content };
24692
+ }
24625
24693
  async function readConfigObject(filePath) {
24626
24694
  if (!await exists(filePath)) await createUserConfig({ path: filePath });
24627
24695
  const raw = JSON.parse(await readFile7(filePath, "utf8"));
@@ -24701,6 +24769,30 @@ function detectDefaultProvider() {
24701
24769
  function objectValue(value) {
24702
24770
  return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
24703
24771
  }
24772
+ function normalizeOptionalName(value) {
24773
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
24774
+ }
24775
+ function newModelProfile(options) {
24776
+ const model = options.model?.trim();
24777
+ if (!model) throw new Error("Model ID is required.");
24778
+ const name = options.name?.trim() || modelProfileNameFromModel(model);
24779
+ return {
24780
+ name,
24781
+ displayName: options.displayName?.trim() || name,
24782
+ model,
24783
+ ...options.baseURL ? { baseURL: options.baseURL } : {},
24784
+ ...options.apiKey !== void 0 || options.apiKeyEnv ? { apiKey: options.apiKey ?? "" } : {},
24785
+ ...options.apiKeyEnv ? { apiKeyEnv: options.apiKeyEnv } : {}
24786
+ };
24787
+ }
24788
+ function modelProfileNameFromModel(model) {
24789
+ return model.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "model";
24790
+ }
24791
+ function setOrDelete(target, key, value) {
24792
+ const trimmed = value.trim();
24793
+ if (trimmed) target[key] = trimmed;
24794
+ else delete target[key];
24795
+ }
24704
24796
  async function exists(filePath) {
24705
24797
  try {
24706
24798
  await stat3(filePath);
@@ -25235,6 +25327,41 @@ async function runConfigCommand(argv) {
25235
25327
  force
25236
25328
  });
25237
25329
  process.stdout.write(`Updated config: ${result.path}
25330
+ `);
25331
+ return 0;
25332
+ }
25333
+ if (command === "set-default-model") {
25334
+ const [provider, ...tail2] = rest;
25335
+ if (!provider) throw new Error("config set-default-model requires a provider name.");
25336
+ const flags = parseFlags(tail2);
25337
+ const result = await setDefaultModelProfile({
25338
+ path: stringFlag(flags, "path"),
25339
+ provider,
25340
+ profileName: stringFlag(flags, "profile"),
25341
+ name: stringFlag(flags, "name"),
25342
+ displayName: stringFlag(flags, "display-name"),
25343
+ model: stringFlag(flags, "model"),
25344
+ baseURL: stringFlag(flags, "base-url"),
25345
+ apiKey: await apiKeyFromFlags(flags),
25346
+ apiKeyEnv: stringFlag(flags, "api-key-env")
25347
+ });
25348
+ process.stdout.write(`Updated config: ${result.path}
25349
+ `);
25350
+ return 0;
25351
+ }
25352
+ if (command === "update-provider") {
25353
+ const [provider, ...tail2] = rest;
25354
+ if (!provider) throw new Error("config update-provider requires a provider name.");
25355
+ const flags = parseFlags(tail2);
25356
+ const result = await updateProviderSettings({
25357
+ path: stringFlag(flags, "path"),
25358
+ provider,
25359
+ baseURL: stringFlagOrEmpty(flags, "base-url"),
25360
+ apiKey: await apiKeyFromFlags(flags),
25361
+ apiKeyEnv: stringFlagOrEmpty(flags, "api-key-env"),
25362
+ authHeader: stringFlagOrEmpty(flags, "auth-header")
25363
+ });
25364
+ process.stdout.write(`Updated config: ${result.path}
25238
25365
  `);
25239
25366
  return 0;
25240
25367
  }
@@ -25332,6 +25459,10 @@ function stringFlag(flags, name) {
25332
25459
  const value = flags.get(name);
25333
25460
  return typeof value === "string" && value.trim() ? value : void 0;
25334
25461
  }
25462
+ function stringFlagOrEmpty(flags, name) {
25463
+ const value = flags.get(name);
25464
+ return typeof value === "string" ? value : void 0;
25465
+ }
25335
25466
  function requiredStringFlag(flags, name) {
25336
25467
  const value = stringFlag(flags, name);
25337
25468
  if (!value) throw new Error(`--${name} is required.`);
@@ -25374,6 +25505,8 @@ Usage:
25374
25505
  demian config setup interactive multi-step provider wizard
25375
25506
  demian config add-provider <preset|openai-compatible> [--name <name>] [--base-url <url>] [--api-key-env <env>] [--api-key-stdin] [--auth-header <header>] [--force]
25376
25507
  demian config add-model <provider> --name <name> --model <model> [--display-name <label>] [--base-url <url>] [--api-key-env <env>] [--api-key-stdin] [--force]
25508
+ demian config set-default-model <provider> [--profile <name> | --model <model>] [--display-name <label>]
25509
+ demian config update-provider <provider> [--base-url <url>] [--api-key-env <env>] [--auth-header <header>]
25377
25510
  demian config models <provider> [--refresh] [--config <path>]
25378
25511
  demian config list [--config <path>]
25379
25512
  demian config path
@@ -31150,6 +31283,9 @@ function providerOptions(config, catalogs = {}) {
31150
31283
  permissionMode: provider.type === "claudecode" ? provider.permissionMode : void 0,
31151
31284
  ga: provider.type === "claudecode" ? claudeCodeGaEnabled(provider) : void 0,
31152
31285
  available,
31286
+ baseURL: typeof provider.baseURL === "string" ? provider.baseURL : void 0,
31287
+ apiKeyEnv: typeof provider.apiKeyEnv === "string" ? provider.apiKeyEnv : void 0,
31288
+ auth: sanitizeProviderAuth(provider.auth),
31153
31289
  catalog: catalog ? { status: catalog.status, source: catalog.source, message: catalog.message } : void 0
31154
31290
  };
31155
31291
  }).sort((left, right) => {
@@ -31224,6 +31360,13 @@ function providerCatalogAvailable(catalog) {
31224
31360
  function unavailableLabel(value) {
31225
31361
  return `(n/a) ${value}`;
31226
31362
  }
31363
+ function sanitizeProviderAuth(auth) {
31364
+ if (!auth || typeof auth !== "object" || Array.isArray(auth)) return void 0;
31365
+ const record = auth;
31366
+ const type = typeof record.type === "string" ? record.type : void 0;
31367
+ const header = typeof record.header === "string" ? record.header : void 0;
31368
+ return type || header ? { type, header } : void 0;
31369
+ }
31227
31370
 
31228
31371
  // src/ui/plain/interactive.ts
31229
31372
  var PlainInteractivePrompt = class {