@minhpnq1807/contextos 0.5.23 → 0.5.24

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.24
4
+
5
+ - **Interactive agent selection:** Replaces the comma-separated text input in `ctx setup` with an interactive multi-select prompt — use ↑/↓ to navigate, Space to toggle agents on/off, and Enter to confirm.
6
+
3
7
  ## 0.5.23
4
8
 
5
9
  - **Fix Windows install paths:** Replaces all `process.env.HOME || process.cwd()` fallbacks with `os.homedir()` across `ctx.js`, `claude-hooks.js`, `antigravity-hooks.js`, `claude-mcp.js`, `antigravity-mcp.js`, and `ruler-sync.js`. On Windows, `HOME` is not set, causing `.codex/`, `.claude/`, and `.gemini/` directories (with full `node_modules` and source code) to be created inside the project tree instead of the user's home directory.
package/bin/ctx.js CHANGED
@@ -30,6 +30,7 @@ import { syncSkills } from "../plugins/ctx/lib/skillshare-sync.js";
30
30
  import { scanSkills, warmSkillEmbeddings } from "../plugins/ctx/lib/skill-discoverer.js";
31
31
  import { parsePassthroughArgs, runPassthrough } from "../plugins/ctx/lib/passthrough.js";
32
32
  import { parseAgentList, parseSetupArgs, setupSummaryLines } from "../plugins/ctx/lib/setup-wizard.js";
33
+ import { multiSelect } from "../plugins/ctx/lib/multi-select.js";
33
34
  import { syncWorkflows, warmWorkflowEmbeddings } from "../plugins/ctx/lib/workflow-discoverer.js";
34
35
 
35
36
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -493,23 +494,40 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
493
494
 
494
495
  if (interactive) {
495
496
  const rl = readline.createInterface({ input, output });
496
- try {
497
- const proceed = await askSetupYesNo(rl, "Install to this directory?", true);
498
- if (!proceed) {
499
- console.log("Setup cancelled.");
500
- return;
497
+ const proceed = await askSetupYesNo(rl, "Install to this directory?", true);
498
+ if (!proceed) {
499
+ rl.close();
500
+ console.log("Setup cancelled.");
501
+ return;
502
+ }
503
+ if (!options.agentsProvided) {
504
+ rl.close();
505
+ const selected = await multiSelect({
506
+ message: "Select agents to install:",
507
+ options: [
508
+ { label: "Codex", value: "codex", selected: options.agents.includes("codex") },
509
+ { label: "Claude", value: "claude", selected: options.agents.includes("claude") },
510
+ { label: "Antigravity (agy)", value: "agy", selected: options.agents.includes("agy") }
511
+ ]
512
+ });
513
+ options.agents = selected;
514
+ const rl2 = readline.createInterface({ input, output });
515
+ try {
516
+ options.inject = await askSetupYesNo(rl2, "Enable prompt context injection?", options.inject);
517
+ options.syncRules = await askSetupYesNo(rl2, "Sync project rules and MCP servers through Ruler?", options.syncRules);
518
+ options.syncSkills = await askSetupYesNo(rl2, "Sync skills through skillshare?", options.syncSkills);
519
+ } finally {
520
+ rl2.close();
501
521
  }
502
- if (!options.agentsProvided) {
503
- const agents = await askSetupQuestion(rl, "Install for agents? comma-separated", options.agents.join(","));
504
- options.agents = parseAgentList(agents);
505
- } else {
522
+ } else {
523
+ try {
506
524
  console.log(`◇ Install for agents:\n│ ${options.agents.join(", ")}`);
525
+ options.inject = await askSetupYesNo(rl, "Enable prompt context injection?", options.inject);
526
+ options.syncRules = await askSetupYesNo(rl, "Sync project rules and MCP servers through Ruler?", options.syncRules);
527
+ options.syncSkills = await askSetupYesNo(rl, "Sync skills through skillshare?", options.syncSkills);
528
+ } finally {
529
+ rl.close();
507
530
  }
508
- options.inject = await askSetupYesNo(rl, "Enable prompt context injection?", options.inject);
509
- options.syncRules = await askSetupYesNo(rl, "Sync project rules and MCP servers through Ruler?", options.syncRules);
510
- options.syncSkills = await askSetupYesNo(rl, "Sync skills through skillshare?", options.syncSkills);
511
- } finally {
512
- rl.close();
513
531
  }
514
532
  }
515
533
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhpnq1807/contextos",
3
- "version": "0.5.23",
3
+ "version": "0.5.24",
4
4
  "description": "Task-aware AGENTS.md context injection and compliance reporting for Codex, Claude Code, and Antigravity.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Interactive multi-select prompt using raw stdin.
3
+ *
4
+ * Usage:
5
+ * const selected = await multiSelect({
6
+ * message: "Select agents:",
7
+ * options: [
8
+ * { label: "Codex", value: "codex", selected: true },
9
+ * { label: "Claude", value: "claude", selected: true },
10
+ * { label: "Antigravity (agy)", value: "agy", selected: true }
11
+ * ]
12
+ * });
13
+ */
14
+
15
+ const KEYS = {
16
+ UP: ["\x1B[A", "\x1Bk"], // Arrow Up, Alt+k
17
+ DOWN: ["\x1B[B", "\x1Bj"], // Arrow Down, Alt+j
18
+ SPACE: [" "],
19
+ ENTER: ["\r", "\n"],
20
+ CTRL_C: ["\x03"],
21
+ // j/k vim-style (single char)
22
+ J: ["j"],
23
+ K: ["k"]
24
+ };
25
+
26
+ const DIM = "\x1B[2m";
27
+ const RESET = "\x1B[0m";
28
+ const CYAN = "\x1B[36m";
29
+ const GREEN = "\x1B[32m";
30
+ const BOLD = "\x1B[1m";
31
+ const HIDE_CURSOR = "\x1B[?25l";
32
+ const SHOW_CURSOR = "\x1B[?25h";
33
+
34
+ function matchKey(data, keySet) {
35
+ return keySet.some((k) => data === k);
36
+ }
37
+
38
+ /**
39
+ * @param {{ message: string, options: Array<{label: string, value: string, selected?: boolean}> }} config
40
+ * @returns {Promise<string[]>} selected values
41
+ */
42
+ export function multiSelect({ message, options }) {
43
+ return new Promise((resolve, reject) => {
44
+ if (!process.stdin.isTTY) {
45
+ // Non-interactive: return all pre-selected
46
+ resolve(options.filter((o) => o.selected).map((o) => o.value));
47
+ return;
48
+ }
49
+
50
+ let cursor = 0;
51
+ const selections = options.map((o) => o.selected !== false);
52
+
53
+ function render() {
54
+ // Move cursor up to overwrite previous render (except first time)
55
+ const lines = [];
56
+ lines.push(`${CYAN}◇${RESET} ${message}`);
57
+ lines.push(`${DIM} Use ↑/↓ to navigate, Space to toggle, Enter to confirm${RESET}`);
58
+ for (let i = 0; i < options.length; i++) {
59
+ const isCursor = i === cursor;
60
+ const isSelected = selections[i];
61
+ const checkbox = isSelected ? `${GREEN}◉${RESET}` : `${DIM}○${RESET}`;
62
+ const label = isCursor ? `${BOLD}${CYAN}${options[i].label}${RESET}` : options[i].label;
63
+ const pointer = isCursor ? `${CYAN}❯${RESET}` : " ";
64
+ lines.push(` ${pointer} ${checkbox} ${label}`);
65
+ }
66
+ return lines;
67
+ }
68
+
69
+ let prevLineCount = 0;
70
+
71
+ function draw() {
72
+ const lines = render();
73
+ // Clear previous output
74
+ if (prevLineCount > 0) {
75
+ process.stdout.write(`\x1B[${prevLineCount}A`); // move up
76
+ for (let i = 0; i < prevLineCount; i++) {
77
+ process.stdout.write("\x1B[2K"); // clear line
78
+ if (i < prevLineCount - 1) process.stdout.write("\x1B[1B"); // move down
79
+ }
80
+ process.stdout.write(`\x1B[${prevLineCount - 1}A`); // back to top
81
+ }
82
+ process.stdout.write(lines.join("\n") + "\n");
83
+ prevLineCount = lines.length;
84
+ }
85
+
86
+ process.stdout.write(HIDE_CURSOR);
87
+
88
+ const wasRaw = process.stdin.isRaw;
89
+ process.stdin.setRawMode(true);
90
+ process.stdin.resume();
91
+ process.stdin.setEncoding("utf8");
92
+
93
+ function cleanup() {
94
+ process.stdin.setRawMode(wasRaw || false);
95
+ process.stdin.pause();
96
+ process.stdin.removeListener("data", onData);
97
+ process.stdout.write(SHOW_CURSOR);
98
+ }
99
+
100
+ function onData(data) {
101
+ if (matchKey(data, KEYS.CTRL_C)) {
102
+ cleanup();
103
+ process.exit(0);
104
+ }
105
+
106
+ if (matchKey(data, KEYS.UP) || matchKey(data, KEYS.K)) {
107
+ cursor = (cursor - 1 + options.length) % options.length;
108
+ draw();
109
+ } else if (matchKey(data, KEYS.DOWN) || matchKey(data, KEYS.J)) {
110
+ cursor = (cursor + 1) % options.length;
111
+ draw();
112
+ } else if (matchKey(data, KEYS.SPACE)) {
113
+ selections[cursor] = !selections[cursor];
114
+ draw();
115
+ } else if (matchKey(data, KEYS.ENTER)) {
116
+ cleanup();
117
+ const selected = options
118
+ .filter((_, i) => selections[i])
119
+ .map((o) => o.value);
120
+ // Print final summary
121
+ const summary = selected.length > 0
122
+ ? selected.join(", ")
123
+ : "(none)";
124
+ // Overwrite prompt area with final state
125
+ if (prevLineCount > 0) {
126
+ process.stdout.write(`\x1B[${prevLineCount}A`);
127
+ for (let i = 0; i < prevLineCount; i++) {
128
+ process.stdout.write("\x1B[2K");
129
+ if (i < prevLineCount - 1) process.stdout.write("\x1B[1B");
130
+ }
131
+ process.stdout.write(`\x1B[${prevLineCount - 1}A`);
132
+ }
133
+ process.stdout.write(`${CYAN}◇${RESET} ${message}\n`);
134
+ process.stdout.write(`${DIM}│${RESET} ${GREEN}${summary}${RESET}\n`);
135
+ resolve(selected);
136
+ }
137
+ }
138
+
139
+ process.stdin.on("data", onData);
140
+ draw();
141
+ });
142
+ }