jinzd-ai-cli 0.1.92 → 0.1.94

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.
@@ -16,7 +16,7 @@ import {
16
16
  SUBAGENT_MAX_ROUNDS_LIMIT,
17
17
  VERSION,
18
18
  runTestsTool
19
- } from "./chunk-W7ZOYZFR.js";
19
+ } from "./chunk-PYJJOWBD.js";
20
20
 
21
21
  // src/config/config-manager.ts
22
22
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -8,7 +8,7 @@ import { platform } from "os";
8
8
  import chalk from "chalk";
9
9
 
10
10
  // src/core/constants.ts
11
- var VERSION = "0.1.92";
11
+ var VERSION = "0.1.94";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  theme,
36
36
  truncateOutput,
37
37
  undoStack
38
- } from "./chunk-OZ2VVEBE.js";
38
+ } from "./chunk-A56ZRY2B.js";
39
39
  import {
40
40
  AGENTIC_BEHAVIOR_GUIDELINE,
41
41
  AUTHOR,
@@ -55,7 +55,7 @@ import {
55
55
  REPO_URL,
56
56
  SKILLS_DIR_NAME,
57
57
  VERSION
58
- } from "./chunk-W7ZOYZFR.js";
58
+ } from "./chunk-PYJJOWBD.js";
59
59
 
60
60
  // src/index.ts
61
61
  import { program } from "commander";
@@ -1904,7 +1904,7 @@ ${hint}` : "")
1904
1904
  description: "Run project tests and show structured report",
1905
1905
  usage: "/test [command|filter]",
1906
1906
  async execute(args, _ctx) {
1907
- const { executeTests } = await import("./run-tests-XWNY6FYC.js");
1907
+ const { executeTests } = await import("./run-tests-56DD3KFS.js");
1908
1908
  const argStr = args.join(" ").trim();
1909
1909
  let testArgs = {};
1910
1910
  if (argStr) {
@@ -5292,7 +5292,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5292
5292
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5293
5293
  process.exit(1);
5294
5294
  }
5295
- const { startWebServer } = await import("./server-Y67I66FG.js");
5295
+ const { startWebServer } = await import("./server-4YKTZGGC.js");
5296
5296
  await startWebServer({ port, host: options.host });
5297
5297
  });
5298
5298
  program.command("sessions").description("List recent conversation sessions").action(async () => {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-W7ZOYZFR.js";
5
+ } from "./chunk-PYJJOWBD.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -23,7 +23,7 @@ import {
23
23
  setupProxy,
24
24
  spawnAgentContext,
25
25
  truncateOutput
26
- } from "./chunk-OZ2VVEBE.js";
26
+ } from "./chunk-A56ZRY2B.js";
27
27
  import {
28
28
  AGENTIC_BEHAVIOR_GUIDELINE,
29
29
  CONTEXT_FILE_CANDIDATES,
@@ -35,7 +35,7 @@ import {
35
35
  PLAN_MODE_SYSTEM_ADDON,
36
36
  SKILLS_DIR_NAME,
37
37
  VERSION
38
- } from "./chunk-W7ZOYZFR.js";
38
+ } from "./chunk-PYJJOWBD.js";
39
39
 
40
40
  // src/web/server.ts
41
41
  import express from "express";
@@ -448,7 +448,6 @@ var SessionHandler = class {
448
448
  this.currentModel = "";
449
449
  }
450
450
  }
451
- this.sessions.createSession(this.currentProvider, this.currentModel);
452
451
  this.activeSystemPrompt = this.loadContextFiles();
453
452
  const hooks = this.config.get("hooks");
454
453
  const permissionRules = this.config.get("permissionRules");
@@ -525,7 +524,19 @@ var SessionHandler = class {
525
524
  if (this.abortController) this.abortController.abort();
526
525
  for (const resolve3 of this.pendingAskUser.values()) resolve3(null);
527
526
  this.pendingAskUser.clear();
528
- this.sessions.save();
527
+ this.saveIfNeeded();
528
+ }
529
+ /** Save session only if it exists and has messages (never persist empty "Untitled" sessions). */
530
+ saveIfNeeded() {
531
+ if (this.sessions.current && this.sessions.current.messages.length > 0) {
532
+ this.sessions.save();
533
+ }
534
+ }
535
+ /** Lazily create a session if none exists yet (deferred from constructor). */
536
+ ensureSession() {
537
+ if (!this.sessions.current) {
538
+ this.sessions.createSession(this.currentProvider, this.currentModel);
539
+ }
529
540
  }
530
541
  // ── Chat handling ────────────────────────────────────────────────
531
542
  async handleChat(content, images) {
@@ -535,6 +546,7 @@ var SessionHandler = class {
535
546
  }
536
547
  this.processing = true;
537
548
  try {
549
+ this.ensureSession();
538
550
  const session = this.sessions.current;
539
551
  let msgContent;
540
552
  if (images && images.length > 0) {
@@ -572,7 +584,9 @@ var SessionHandler = class {
572
584
  this.send({ type: "error", message });
573
585
  } finally {
574
586
  this.processing = false;
587
+ this.saveIfNeeded();
575
588
  this.sendStatus();
589
+ this.sendSessionList();
576
590
  }
577
591
  }
578
592
  async handleChatSimple(provider, messages) {
@@ -865,10 +879,12 @@ ${summaryResult.content}`,
865
879
  break;
866
880
  }
867
881
  case "clear":
882
+ this.saveIfNeeded();
868
883
  this.sessions.createSession(this.currentProvider, this.currentModel);
869
884
  this.sessionTokenUsage = { inputTokens: 0, outputTokens: 0 };
870
885
  this.send({ type: "info", message: "Conversation cleared." });
871
886
  this.sendStatus();
887
+ this.sendSessionList();
872
888
  break;
873
889
  case "compact":
874
890
  await this.compactSession(args.join(" ") || void 0);
@@ -911,7 +927,7 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
911
927
  case "session": {
912
928
  const sub = args[0];
913
929
  if (sub === "new") {
914
- this.sessions.save();
930
+ this.saveIfNeeded();
915
931
  this.sessions.createSession(this.currentProvider, this.currentModel);
916
932
  this.sessionTokenUsage = { inputTokens: 0, outputTokens: 0 };
917
933
  this.send({ type: "info", message: "New session created." });
@@ -919,7 +935,7 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
919
935
  this.sendSessionList();
920
936
  } else if (sub === "load" && args[1]) {
921
937
  const targetId = args[1];
922
- this.sessions.save();
938
+ this.saveIfNeeded();
923
939
  const list = this.sessions.listSessions();
924
940
  const found = list.find((s) => s.id.startsWith(targetId));
925
941
  if (found) {
@@ -947,6 +963,23 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
947
963
  } else {
948
964
  this.send({ type: "error", message: `Session not found: ${targetId}` });
949
965
  }
966
+ } else if (sub === "batch-delete") {
967
+ const ids = args.slice(1).filter(Boolean);
968
+ if (ids.length === 0) {
969
+ this.send({ type: "error", message: "No session IDs provided." });
970
+ break;
971
+ }
972
+ const list = this.sessions.listSessions();
973
+ let deleted = 0;
974
+ for (const targetId of ids) {
975
+ const found = list.find((s) => s.id.startsWith(targetId));
976
+ if (found) {
977
+ this.sessions.deleteSession(found.id);
978
+ deleted++;
979
+ }
980
+ }
981
+ this.send({ type: "info", message: `Deleted ${deleted} session(s).` });
982
+ this.sendSessionList();
950
983
  } else {
951
984
  this.send({ type: "info", message: "Usage: /session new | list | load <id> | delete <id>" });
952
985
  }
@@ -972,6 +1005,9 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
972
1005
  " /cost \u2014 Show cumulative token usage",
973
1006
  " /tools \u2014 Show tools, MCP servers & skills in sidebar",
974
1007
  " /export [md|json] \u2014 Export conversation as Markdown or JSON",
1008
+ " /skill \u2014 List available skills",
1009
+ " /skill <name> \u2014 Activate a skill",
1010
+ " /skill off \u2014 Deactivate current skill",
975
1011
  " /memory \u2014 Show persistent memory contents",
976
1012
  " /memory add <text> \u2014 Add entry to persistent memory",
977
1013
  " /memory clear \u2014 Clear persistent memory",
@@ -1020,6 +1056,49 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1020
1056
  }
1021
1057
  break;
1022
1058
  }
1059
+ case "skill": {
1060
+ const sub = args[0];
1061
+ if (!this.skillManager) {
1062
+ this.send({ type: "error", message: "Skill system not available." });
1063
+ break;
1064
+ }
1065
+ if (!sub || sub === "list") {
1066
+ const skills = this.skillManager.listSkills();
1067
+ const active = this.skillManager.getActive();
1068
+ if (skills.length === 0) {
1069
+ this.send({ type: "info", message: "No skills available. Add .md files to ~/.aicli/skills/" });
1070
+ } else {
1071
+ const lines = skills.map((s) => {
1072
+ const isActive = active?.meta.name === s.meta.name;
1073
+ const marker = isActive ? " \u2705 (active)" : "";
1074
+ return ` ${s.meta.name}${marker} \u2014 ${s.meta.description || "(no description)"}`;
1075
+ });
1076
+ this.send({ type: "info", message: `\u{1F4DA} Skills:
1077
+ ${lines.join("\n")}` });
1078
+ }
1079
+ } else if (sub === "off") {
1080
+ this.skillManager.deactivate();
1081
+ this.send({ type: "info", message: "\u{1F50C} Skill deactivated." });
1082
+ this.sendToolsList();
1083
+ this.sendStatus();
1084
+ } else if (sub === "reload") {
1085
+ this.skillManager.loadSkills();
1086
+ this.send({ type: "info", message: `\u{1F504} Reloaded ${this.skillManager.listSkills().length} skill(s).` });
1087
+ this.sendToolsList();
1088
+ } else {
1089
+ const activated = this.skillManager.activate(sub);
1090
+ if (activated) {
1091
+ this.send({ type: "info", message: `\u2705 Skill activated: ${activated.meta.name}
1092
+ ${activated.meta.description || ""}` });
1093
+ this.sendToolsList();
1094
+ this.sendStatus();
1095
+ } else {
1096
+ const available = this.skillManager.listSkills().map((s) => s.meta.name).join(", ");
1097
+ this.send({ type: "error", message: `Skill "${sub}" not found. Available: ${available}` });
1098
+ }
1099
+ }
1100
+ break;
1101
+ }
1023
1102
  default:
1024
1103
  this.send({ type: "error", message: `Unknown command: /${name}. Type /help for available commands.` });
1025
1104
  }
@@ -741,6 +741,9 @@ function renderSessionList(sessions) {
741
741
  renderFilteredSessions(sessionSearchInput?.value || '');
742
742
  }
743
743
 
744
+ let batchSelectMode = false;
745
+ const batchSelectedIds = new Set();
746
+
744
747
  function renderFilteredSessions(filter) {
745
748
  const sessions = filter
746
749
  ? cachedSessions.filter(s => (s.title || '').toLowerCase().includes(filter.toLowerCase()))
@@ -753,37 +756,91 @@ function renderFilteredSessions(filter) {
753
756
  const title = s.title || 'Untitled';
754
757
  const date = new Date(s.updated);
755
758
  const timeStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
759
+ const checked = batchSelectedIds.has(s.id) ? 'checked' : '';
760
+ const checkbox = batchSelectMode
761
+ ? `<input type="checkbox" class="checkbox checkbox-xs checkbox-error session-batch-cb" data-sid="${s.id}" ${checked}>`
762
+ : '';
756
763
  return `<div class="session-item ${s.isCurrent ? 'active' : ''}" data-session-id="${s.id}" title="${escapeHtml(title)}">
757
764
  <div class="flex items-center gap-1">
765
+ ${checkbox}
758
766
  <div class="session-title flex-1">${escapeHtml(title)}</div>
759
- <button class="session-delete-btn opacity-0 hover:opacity-100 text-error text-xs px-1 flex-shrink-0" data-delete-id="${s.id}" title="Delete session">&times;</button>
767
+ ${batchSelectMode ? '' : `<button class="session-delete-btn opacity-0 hover:opacity-100 text-error text-xs px-1 flex-shrink-0" data-delete-id="${s.id}" title="Delete session">&times;</button>`}
760
768
  </div>
761
769
  <div class="session-meta">${s.messageCount} msgs · ${timeStr}</div>
762
770
  </div>`;
763
771
  }).join('');
764
772
 
765
- // Click to load session
773
+ // Click to load session (only in normal mode)
766
774
  sessionListEl.querySelectorAll('.session-item').forEach(el => {
767
775
  el.addEventListener('click', (e) => {
768
- // Don't load if clicking the delete button
769
- if (e.target.closest('.session-delete-btn')) return;
776
+ if (e.target.closest('.session-delete-btn') || e.target.closest('.session-batch-cb')) return;
777
+ if (batchSelectMode) {
778
+ // In batch mode, clicking the row toggles the checkbox
779
+ const cb = el.querySelector('.session-batch-cb');
780
+ if (cb) { cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); }
781
+ return;
782
+ }
770
783
  const id = el.dataset.sessionId;
771
784
  if (!id) return;
772
785
  send({ type: 'command', name: 'session', args: ['load', id] });
773
786
  });
774
787
  });
775
788
 
776
- // Click to delete session
777
- sessionListEl.querySelectorAll('.session-delete-btn').forEach(btn => {
778
- btn.addEventListener('click', (e) => {
779
- e.stopPropagation();
780
- const id = btn.dataset.deleteId;
781
- if (!id) return;
782
- if (confirm('Delete this session?')) {
783
- send({ type: 'command', name: 'session', args: ['delete', id] });
784
- }
789
+ // Checkbox change in batch mode
790
+ sessionListEl.querySelectorAll('.session-batch-cb').forEach(cb => {
791
+ cb.addEventListener('change', () => {
792
+ const sid = cb.dataset.sid;
793
+ if (cb.checked) batchSelectedIds.add(sid); else batchSelectedIds.delete(sid);
794
+ updateBatchBar();
785
795
  });
786
796
  });
797
+
798
+ // Click to delete session (normal mode only)
799
+ if (!batchSelectMode) {
800
+ sessionListEl.querySelectorAll('.session-delete-btn').forEach(btn => {
801
+ btn.addEventListener('click', (e) => {
802
+ e.stopPropagation();
803
+ const id = btn.dataset.deleteId;
804
+ if (!id) return;
805
+ if (confirm('Delete this session?')) {
806
+ send({ type: 'command', name: 'session', args: ['delete', id] });
807
+ }
808
+ });
809
+ });
810
+ }
811
+ }
812
+
813
+ function toggleBatchSelect() {
814
+ batchSelectMode = !batchSelectMode;
815
+ batchSelectedIds.clear();
816
+ updateBatchBar();
817
+ renderFilteredSessions(sessionSearchInput?.value || '');
818
+ }
819
+
820
+ function selectAllSessions() {
821
+ cachedSessions.forEach(s => batchSelectedIds.add(s.id));
822
+ renderFilteredSessions(sessionSearchInput?.value || '');
823
+ updateBatchBar();
824
+ }
825
+
826
+ function batchDeleteSelected() {
827
+ if (batchSelectedIds.size === 0) return;
828
+ if (!confirm(`Delete ${batchSelectedIds.size} session(s)?`)) return;
829
+ send({ type: 'command', name: 'session', args: ['batch-delete', ...batchSelectedIds] });
830
+ batchSelectMode = false;
831
+ batchSelectedIds.clear();
832
+ updateBatchBar();
833
+ }
834
+
835
+ function updateBatchBar() {
836
+ const bar = document.getElementById('batch-bar');
837
+ if (!bar) return;
838
+ if (batchSelectMode) {
839
+ bar.classList.remove('hidden');
840
+ bar.querySelector('.batch-count').textContent = `${batchSelectedIds.size} selected`;
841
+ } else {
842
+ bar.classList.add('hidden');
843
+ }
787
844
  }
788
845
 
789
846
  function renderSessionMessages(messages) {
@@ -49,7 +49,7 @@
49
49
  <div class="flex flex-1 overflow-hidden">
50
50
 
51
51
  <!-- Sidebar -->
52
- <aside id="sidebar" class="sidebar bg-base-200 border-r border-base-content/10 flex flex-col w-64 flex-shrink-0 overflow-hidden transition-all duration-200">
52
+ <aside id="sidebar" class="sidebar bg-base-200 border-r border-base-content/10 flex flex-col w-72 flex-shrink-0 overflow-hidden transition-all duration-200">
53
53
  <!-- Sidebar tabs -->
54
54
  <div class="flex border-b border-base-content/10 flex-shrink-0">
55
55
  <button class="sidebar-tab active flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="sessions">📋 Sessions</button>
@@ -58,9 +58,18 @@
58
58
  </div>
59
59
  <!-- Sessions tab -->
60
60
  <div id="tab-sessions" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden">
61
- <div class="p-2 border-b border-base-content/10 flex items-center justify-between">
62
- <input id="session-search" type="text" class="input input-xs input-bordered flex-1 mr-2" placeholder="Search sessions...">
63
- <button id="btn-new-session" class="btn btn-xs btn-primary btn-outline flex-shrink-0" title="New session">+ New</button>
61
+ <div class="p-2 border-b border-base-content/10 flex items-center justify-between gap-1">
62
+ <input id="session-search" type="text" class="input input-xs input-bordered flex-1 min-w-0" placeholder="Search...">
63
+ <button id="btn-batch-select" class="btn btn-xs btn-ghost flex-shrink-0" title="Select multiple" onclick="toggleBatchSelect()">☑</button>
64
+ <button id="btn-new-session" class="btn btn-xs btn-primary btn-outline flex-shrink-0 whitespace-nowrap" title="New session">+ New</button>
65
+ </div>
66
+ <!-- Batch action bar (hidden by default) -->
67
+ <div id="batch-bar" class="hidden px-2 py-1 border-b border-base-content/10 flex items-center gap-1 bg-base-300 text-xs">
68
+ <span class="batch-count opacity-60">0 selected</span>
69
+ <span class="flex-1"></span>
70
+ <button class="btn btn-xs btn-ghost" onclick="selectAllSessions()">All</button>
71
+ <button class="btn btn-xs btn-error btn-outline" onclick="batchDeleteSelected()">🗑 Delete</button>
72
+ <button class="btn btn-xs btn-ghost" onclick="toggleBatchSelect()">Cancel</button>
64
73
  </div>
65
74
  <div id="session-list" class="flex-1 overflow-y-auto p-2 flex flex-col gap-1 text-sm">
66
75
  <div class="text-xs opacity-40 text-center py-4">No sessions yet</div>
@@ -562,7 +562,7 @@
562
562
  /* ── Responsive ─────────────────────────────────────── */
563
563
  @media (max-width: 768px) {
564
564
  .sidebar { width: 0; padding: 0; border: none; }
565
- .sidebar.sidebar-open { width: 16rem; position: absolute; z-index: 20; height: calc(100vh - 3.5rem); top: 3.5rem; }
565
+ .sidebar.sidebar-open { width: 18rem; position: absolute; z-index: 20; height: calc(100vh - 3.5rem); top: 3.5rem; }
566
566
  }
567
567
  @media (max-width: 640px) {
568
568
  .navbar-start .select { width: 6rem; font-size: 0.75rem; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.92",
3
+ "version": "0.1.94",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",