wispy-cli 2.7.15 → 2.7.17

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/bin/wispy.mjs CHANGED
@@ -56,6 +56,7 @@ function hasFlag(flag) {
56
56
 
57
57
  // Parse global flags (order doesn't matter, remove from args so REPL doesn't see them)
58
58
  const globalProfile = extractFlag(["--profile", "-p"], true);
59
+ const globalWorkstream = extractFlag(["--workstream", "-w"], true);
59
60
  const globalPersonality = extractFlag(["--personality"], true);
60
61
  const globalJsonMode = hasFlag("--json");
61
62
  if (globalJsonMode) { args.splice(args.indexOf("--json"), 1); }
@@ -86,6 +87,10 @@ const imagePaths = [];
86
87
 
87
88
  // Expose for submodules via env
88
89
  if (globalProfile) process.env.WISPY_PROFILE = globalProfile;
90
+ // Validate workstream: ignore flag-like values (e.g. --invalid-arg)
91
+ if (globalWorkstream && !globalWorkstream.startsWith("-")) {
92
+ process.env.WISPY_WORKSTREAM = globalWorkstream;
93
+ }
89
94
  if (globalPersonality) process.env.WISPY_PERSONALITY = globalPersonality;
90
95
  if (imagePaths.length > 0) process.env.WISPY_IMAGES = JSON.stringify(imagePaths);
91
96
  if (globalSystemPrompt) process.env.WISPY_SYSTEM_PROMPT = globalSystemPrompt;
@@ -757,12 +762,31 @@ if (command === "exec") {
757
762
  const disallowedTools = globalDisallowedTools || null;
758
763
  const agent = globalAgent || null;
759
764
 
765
+ // Load project settings and merge
766
+ const { findProjectSettings, mergeSettings } = await import(join(rootDir, "core/project-settings.mjs"));
767
+ const projectSettings = await findProjectSettings();
768
+ const mergedConfig = mergeSettings(
769
+ profileConfig,
770
+ projectSettings,
771
+ null, // profile already merged into profileConfig via loadConfigWithProfile
772
+ {
773
+ ...(personality ? { personality } : {}),
774
+ ...(effort ? { effort } : {}),
775
+ ...(maxBudgetUsd ? { maxBudgetUsd } : {}),
776
+ ...(globalSystemPrompt ? { systemPrompt: globalSystemPrompt } : {}),
777
+ ...(globalAppendSystemPrompt ? { appendSystemPrompt: globalAppendSystemPrompt } : {}),
778
+ }
779
+ );
780
+
760
781
  const engine = new WispyEngine({
761
- personality,
762
- effort,
763
- maxBudgetUsd,
764
- allowedTools,
765
- disallowedTools,
782
+ ...mergedConfig,
783
+ personality: mergedConfig.personality ?? personality,
784
+ effort: mergedConfig.effort ?? effort,
785
+ maxBudgetUsd: mergedConfig.maxBudgetUsd ?? maxBudgetUsd,
786
+ allowedTools: mergedConfig.allowedTools ?? allowedTools,
787
+ disallowedTools: mergedConfig.disallowedTools ?? disallowedTools,
788
+ systemPrompt: mergedConfig.systemPrompt ?? null,
789
+ appendSystemPrompt: mergedConfig.appendSystemPrompt ?? null,
766
790
  workstream: process.env.WISPY_WORKSTREAM ?? "default",
767
791
  });
768
792
 
@@ -789,6 +813,7 @@ if (command === "exec") {
789
813
  personality,
790
814
  effort,
791
815
  agent,
816
+ sessionName: globalSessionName ?? null,
792
817
  skipSkillCapture: true,
793
818
  skipUserModel: true,
794
819
  });
@@ -976,7 +1001,8 @@ if (command === "sessions") {
976
1001
  for (const s of sessions) {
977
1002
  const ts = new Date(s.updatedAt).toLocaleString();
978
1003
  const preview = s.firstMessage ? ` "${s.firstMessage.slice(0, 60)}${s.firstMessage.length > 60 ? "…" : ""}"` : "";
979
- console.log(` ${s.id}`);
1004
+ const nameLabel = s.name ? ` · 📎 ${s.name}` : "";
1005
+ console.log(` ${s.id}${nameLabel}`);
980
1006
  console.log(` ${ts} · ${s.workstream} · ${s.messageCount} msgs${s.model ? ` · ${s.model}` : ""}`);
981
1007
  if (preview) console.log(` ${preview}`);
982
1008
  console.log("");
@@ -987,7 +1013,7 @@ if (command === "sessions") {
987
1013
  const choices = [
988
1014
  { name: "(exit)", value: null },
989
1015
  ...sessions.map(s => ({
990
- name: `${s.id} ${new Date(s.updatedAt).toLocaleString()} "${(s.firstMessage ?? "").slice(0, 50)}"`,
1016
+ name: `${s.id}${s.name ? ` [${s.name}]` : ""} ${new Date(s.updatedAt).toLocaleString()} "${(s.firstMessage ?? "").slice(0, 50)}"`,
991
1017
  value: s.id,
992
1018
  })),
993
1019
  ];
@@ -1056,7 +1082,7 @@ if (command === "resume") {
1056
1082
  }
1057
1083
 
1058
1084
  const choices = sessions.map(s => ({
1059
- name: `${new Date(s.updatedAt).toLocaleString()} [${s.workstream}] ${s.messageCount}msgs "${(s.firstMessage ?? "").slice(0, 50)}"`,
1085
+ name: `${new Date(s.updatedAt).toLocaleString()} [${s.workstream}]${s.name ? ` 📎 ${s.name}` : ""} ${s.messageCount}msgs "${(s.firstMessage ?? "").slice(0, 50)}"`,
1060
1086
  value: s.id,
1061
1087
  }));
1062
1088
 
@@ -1105,7 +1131,7 @@ if (command === "fork") {
1105
1131
  }
1106
1132
 
1107
1133
  const choices = sessions.map(s => ({
1108
- name: `${new Date(s.updatedAt).toLocaleString()} [${s.workstream}] ${s.messageCount}msgs "${(s.firstMessage ?? "").slice(0, 50)}"`,
1134
+ name: `${new Date(s.updatedAt).toLocaleString()} [${s.workstream}]${s.name ? ` 📎 ${s.name}` : ""} ${s.messageCount}msgs "${(s.firstMessage ?? "").slice(0, 50)}"`,
1109
1135
  value: s.id,
1110
1136
  }));
1111
1137
 
package/core/engine.mjs CHANGED
@@ -96,12 +96,22 @@ export class WispyEngine {
96
96
  this._workMdContent = null;
97
97
  this._workMdLoaded = false;
98
98
  this._workMdPath = null;
99
- this._activeWorkstream = config.workstream
100
- ?? process.env.WISPY_WORKSTREAM
101
- ?? process.argv.find((a, i) => (process.argv[i-1] === "-w" || process.argv[i-1] === "--workstream"))
102
- ?? "default";
99
+ {
100
+ const _ws = config.workstream
101
+ ?? process.env.WISPY_WORKSTREAM
102
+ ?? process.argv.find((a, i) => (process.argv[i-1] === "-w" || process.argv[i-1] === "--workstream"))
103
+ ?? "default";
104
+ // Sanitize: never use a flag-like value as workstream name
105
+ this._activeWorkstream = (_ws && !_ws.startsWith("-")) ? _ws : "default";
106
+ }
103
107
  // Personality: from config, or null (use default Wispy personality)
104
108
  this._personality = config.personality ?? null;
109
+ // System prompt overrides from config
110
+ this._systemPrompt = config.systemPrompt ?? null; // full replacement
111
+ this._appendSystemPrompt = config.appendSystemPrompt ?? null; // append to default
112
+ // Project settings cache (loaded lazily)
113
+ this._projectSettings = null;
114
+ this._projectSettingsLoaded = false;
105
115
  // Agent manager
106
116
  this.agentManager = new AgentManager(config);
107
117
  // Effort level: low | medium | high | max
@@ -29,9 +29,11 @@ async function readJsonOr(filePath, fallback = null) {
29
29
 
30
30
  async function getActiveWorkstream() {
31
31
  const envWs = process.env.WISPY_WORKSTREAM;
32
- if (envWs) return envWs;
32
+ if (envWs && !envWs.startsWith("-")) return envWs;
33
33
  const cfg = await readJsonOr(CONFIG_PATH, {});
34
- return cfg.workstream ?? "default";
34
+ const ws = cfg.workstream ?? "default";
35
+ // Sanitize: never return a flag-like value (e.g. "--invalid-arg")
36
+ return (ws && !ws.startsWith("-")) ? ws : "default";
35
37
  }
36
38
 
37
39
  async function getProviderLabel(cfg) {
@@ -62,8 +62,10 @@ function box(lines, { padding = 1 } = {}) {
62
62
  // ---------------------------------------------------------------------------
63
63
 
64
64
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
65
- const ACTIVE_WORKSTREAM = process.env.WISPY_WORKSTREAM ??
65
+ const _rawWorkstream = process.env.WISPY_WORKSTREAM ??
66
66
  process.argv.find((a, i) => (process.argv[i-1] === "-w" || process.argv[i-1] === "--workstream")) ?? "default";
67
+ // Validate: ignore flag-like values to avoid e.g. "--invalid-arg" becoming workstream
68
+ const ACTIVE_WORKSTREAM = (_rawWorkstream && !_rawWorkstream.startsWith("-")) ? _rawWorkstream : "default";
67
69
  const HISTORY_FILE = path.join(CONVERSATIONS_DIR, `${ACTIVE_WORKSTREAM}.json`);
68
70
 
69
71
  async function readFileOr(filePath, fallback = null) {
@@ -327,6 +329,7 @@ ${bold("Wispy Commands:")}
327
329
  ${cyan("/search")} <keyword> Search across workstreams
328
330
  ${cyan("/skills")} List installed skills
329
331
  ${cyan("/sessions")} List sessions
332
+ ${cyan("/name")} <name> Give this session a display name (e.g. /name refactor-auth)
330
333
  ${cyan("/mcp")} [list|connect|disconnect|config|reload] MCP management
331
334
  ${cyan("/remember")} <text> Save text to main memory (MEMORY.md)
332
335
  ${cyan("/forget")} <key> Delete a memory file
@@ -1069,6 +1072,28 @@ ${bold("Permissions & Audit (v1.1):")}
1069
1072
  return true;
1070
1073
  }
1071
1074
 
1075
+ if (cmd.startsWith("/name")) {
1076
+ const nameParts = input.split(" ").slice(1);
1077
+ const sessionName = nameParts.join(" ").trim();
1078
+ if (!sessionName) {
1079
+ console.log(yellow("Usage: /name <display-name> (e.g. /name refactor-auth)"));
1080
+ return true;
1081
+ }
1082
+ // Find the active session and set its name
1083
+ try {
1084
+ const currentSession = engine.sessions?.list?.()?.find(Boolean);
1085
+ if (currentSession) {
1086
+ await engine.sessions.setName(currentSession.id, sessionName);
1087
+ console.log(green(`📎 Session named: "${sessionName}" (${currentSession.id})`));
1088
+ } else {
1089
+ console.log(yellow("No active session found."));
1090
+ }
1091
+ } catch (err) {
1092
+ console.log(red(`Failed to set session name: ${err.message}`));
1093
+ }
1094
+ return true;
1095
+ }
1096
+
1072
1097
  if (cmd === "/quit" || cmd === "/exit") {
1073
1098
  console.log(dim(`🌿 Bye! (${engine.providers.formatCost()})`));
1074
1099
  engine.destroy();
@@ -1293,6 +1318,7 @@ async function runRepl(engine) {
1293
1318
  noSave: true,
1294
1319
  dryRun: engine.dryRunMode ?? false,
1295
1320
  images: pendingImages,
1321
+ sessionName: process.env.WISPY_SESSION_NAME ?? null,
1296
1322
  onSkillLearned: (skill) => {
1297
1323
  console.log(cyan(`\n💡 Learned new skill: '${skill.name}' — use /${skill.name} next time`));
1298
1324
  },
@@ -1476,8 +1502,29 @@ if (args[0] && operatorCommands.has(args[0])) {
1476
1502
  process.exit(0);
1477
1503
  }
1478
1504
 
1479
- // Initialize engine
1480
- const engine = new WispyEngine({ workstream: ACTIVE_WORKSTREAM });
1505
+ // Initialize engine — merge project settings + env flags
1506
+ let _engineConfig = { workstream: ACTIVE_WORKSTREAM };
1507
+ try {
1508
+ const { findProjectSettings, mergeSettings } = await import("../core/project-settings.mjs");
1509
+ const { loadConfigWithProfile } = await import("../core/config.mjs");
1510
+ const projectSettings = await findProjectSettings();
1511
+ if (projectSettings) {
1512
+ const baseConfig = await loadConfigWithProfile(process.env.WISPY_PROFILE ?? null);
1513
+ _engineConfig = mergeSettings(baseConfig, projectSettings, null, {
1514
+ ...(process.env.WISPY_SYSTEM_PROMPT ? { systemPrompt: process.env.WISPY_SYSTEM_PROMPT } : {}),
1515
+ ...(process.env.WISPY_APPEND_SYSTEM_PROMPT ? { appendSystemPrompt: process.env.WISPY_APPEND_SYSTEM_PROMPT } : {}),
1516
+ ...(process.env.WISPY_EFFORT ? { effort: process.env.WISPY_EFFORT } : {}),
1517
+ });
1518
+ _engineConfig.workstream = _engineConfig.workstream ?? ACTIVE_WORKSTREAM;
1519
+ } else {
1520
+ // No project settings, still pick up env flags
1521
+ if (process.env.WISPY_SYSTEM_PROMPT) _engineConfig.systemPrompt = process.env.WISPY_SYSTEM_PROMPT;
1522
+ if (process.env.WISPY_APPEND_SYSTEM_PROMPT) _engineConfig.appendSystemPrompt = process.env.WISPY_APPEND_SYSTEM_PROMPT;
1523
+ if (process.env.WISPY_EFFORT) _engineConfig.effort = process.env.WISPY_EFFORT;
1524
+ }
1525
+ } catch { /* project settings are optional */ }
1526
+
1527
+ const engine = new WispyEngine(_engineConfig);
1481
1528
  const initResult = await engine.init();
1482
1529
 
1483
1530
  if (!initResult) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wispy-cli",
3
- "version": "2.7.15",
3
+ "version": "2.7.17",
4
4
  "description": "🌿 Wispy — AI workspace assistant with trustworthy execution (harness, receipts, approvals, diffs)",
5
5
  "license": "MIT",
6
6
  "author": "Minseo & Poropo",
@@ -59,7 +59,8 @@
59
59
  "scripts": {
60
60
  "test": "node --test --test-force-exit --test-timeout=15000 tests/*.test.mjs",
61
61
  "test:basic": "node --test --test-force-exit test/basic.test.mjs",
62
- "test:verbose": "node --test --test-force-exit --test-timeout=15000 --test-reporter=spec tests/*.test.mjs"
62
+ "test:verbose": "node --test --test-force-exit --test-timeout=15000 --test-reporter=spec tests/*.test.mjs",
63
+ "postpublish": "npm install -g wispy-cli@latest || true"
63
64
  },
64
65
  "dependencies": {
65
66
  "@inquirer/prompts": "^8.3.2",