juno-code 1.0.47 → 1.0.50

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.
Files changed (60) hide show
  1. package/README.md +455 -205
  2. package/dist/bin/cli.d.mts +17 -0
  3. package/dist/bin/cli.d.ts +17 -0
  4. package/dist/bin/cli.js +6456 -17604
  5. package/dist/bin/cli.js.map +1 -1
  6. package/dist/bin/cli.mjs +6443 -17589
  7. package/dist/bin/cli.mjs.map +1 -1
  8. package/dist/bin/feedback-collector.d.mts +2 -0
  9. package/dist/bin/feedback-collector.d.ts +2 -0
  10. package/dist/bin/feedback-collector.js.map +1 -1
  11. package/dist/bin/feedback-collector.mjs.map +1 -1
  12. package/dist/index.d.mts +2133 -0
  13. package/dist/index.d.ts +2133 -0
  14. package/dist/index.js +3916 -14711
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +3914 -14516
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/templates/extensions/pi/juno-skill-preprocessor.ts +239 -0
  19. package/dist/templates/scripts/__pycache__/github.cpython-313.pyc +0 -0
  20. package/dist/templates/scripts/__pycache__/parallel_runner.cpython-313.pyc +0 -0
  21. package/dist/templates/scripts/__pycache__/slack_respond.cpython-313.pyc +0 -0
  22. package/dist/templates/scripts/install_requirements.sh +41 -3
  23. package/dist/templates/scripts/kanban.sh +22 -4
  24. package/dist/templates/scripts/parallel_runner.sh +2242 -0
  25. package/dist/templates/services/README.md +61 -1
  26. package/dist/templates/services/__pycache__/claude.cpython-313.pyc +0 -0
  27. package/dist/templates/services/__pycache__/codex.cpython-313.pyc +0 -0
  28. package/dist/templates/services/__pycache__/pi.cpython-313.pyc +0 -0
  29. package/dist/templates/services/claude.py +132 -33
  30. package/dist/templates/services/codex.py +179 -66
  31. package/dist/templates/services/gemini.py +117 -27
  32. package/dist/templates/services/pi.py +2796 -0
  33. package/dist/templates/skills/claude/kanban-workflow/SKILL.md +138 -0
  34. package/dist/templates/skills/claude/plan-kanban-tasks/SKILL.md +15 -8
  35. package/dist/templates/skills/claude/ralph-loop/SKILL.md +18 -22
  36. package/dist/templates/skills/claude/ralph-loop/references/first_check.md +15 -14
  37. package/dist/templates/skills/claude/ralph-loop/references/implement.md +17 -17
  38. package/dist/templates/skills/claude/ralph-loop/scripts/kanban.sh +22 -4
  39. package/dist/templates/skills/claude/understand-project/SKILL.md +15 -8
  40. package/dist/templates/skills/codex/kanban-workflow/SKILL.md +139 -0
  41. package/dist/templates/skills/codex/plan-kanban-tasks/SKILL.md +32 -0
  42. package/dist/templates/skills/codex/ralph-loop/SKILL.md +18 -22
  43. package/dist/templates/skills/codex/ralph-loop/references/first_check.md +15 -14
  44. package/dist/templates/skills/codex/ralph-loop/references/implement.md +17 -17
  45. package/dist/templates/skills/codex/ralph-loop/scripts/kanban.sh +22 -4
  46. package/dist/templates/skills/codex/understand-project/SKILL.md +46 -0
  47. package/dist/templates/skills/pi/.gitkeep +0 -0
  48. package/dist/templates/skills/pi/kanban-workflow/SKILL.md +139 -0
  49. package/dist/templates/skills/pi/plan-kanban-tasks/SKILL.md +32 -0
  50. package/dist/templates/skills/pi/ralph-loop/SKILL.md +43 -0
  51. package/dist/templates/skills/pi/ralph-loop/references/first_check.md +21 -0
  52. package/dist/templates/skills/pi/ralph-loop/references/implement.md +99 -0
  53. package/dist/templates/skills/pi/understand-project/SKILL.md +46 -0
  54. package/package.json +26 -46
  55. package/dist/templates/scripts/__pycache__/attachment_downloader.cpython-38.pyc +0 -0
  56. package/dist/templates/scripts/__pycache__/github.cpython-38.pyc +0 -0
  57. package/dist/templates/scripts/__pycache__/slack_fetch.cpython-38.pyc +0 -0
  58. package/dist/templates/scripts/__pycache__/slack_state.cpython-38.pyc +0 -0
  59. package/dist/templates/services/__pycache__/claude.cpython-38.pyc +0 -0
  60. package/dist/templates/services/__pycache__/codex.cpython-38.pyc +0 -0
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Juno Skill Preprocessor — Pi Extension
3
+ *
4
+ * Adds variable substitution ($1, $2, $ARGUMENTS, $@, ${@:N}, ${@:N:L})
5
+ * and shell directive execution (!`command`) to Pi skill invocations.
6
+ *
7
+ * This extension intercepts /skill: commands via the "input" event,
8
+ * BEFORE Pi's internal _expandSkillCommand() runs. It reads the skill
9
+ * file, processes variables and shell directives, wraps the result in
10
+ * <skill> tags, and returns the fully expanded text.
11
+ *
12
+ * Shell directives only execute when the skill's frontmatter contains:
13
+ * enable-shell-directives: true
14
+ *
15
+ * Variable substitution always runs when arguments are provided.
16
+ *
17
+ * Shipped via juno-code's SkillInstaller to .pi/extensions/.
18
+ */
19
+ import type { ExtensionAPI, InputEvent } from "@mariozechner/pi-coding-agent";
20
+ import { execSync } from "child_process";
21
+ import { existsSync, readFileSync } from "fs";
22
+ import { dirname, join } from "path";
23
+
24
+ const SHELL_DIRECTIVE_REGEX = /!`([^`]+)`/g;
25
+ const DEFAULT_SHELL_TIMEOUT = 5000;
26
+
27
+ /** Directories to search for skill files, relative to project root. */
28
+ const SKILL_DIRS = [".pi/skills", ".claude/skills"];
29
+
30
+ /**
31
+ * Find a skill's SKILL.md file by name, searching known skill directories.
32
+ * Checks both {dir}/{name}/SKILL.md and {dir}/{name}.md patterns.
33
+ */
34
+ function findSkillFile(skillName: string, cwd: string): string | null {
35
+ for (const dir of SKILL_DIRS) {
36
+ const candidates = [join(cwd, dir, skillName, "SKILL.md"), join(cwd, dir, `${skillName}.md`)];
37
+ for (const candidate of candidates) {
38
+ if (existsSync(candidate)) return candidate;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * Parse YAML-like frontmatter from a skill file.
46
+ * Returns frontmatter key-value pairs and the body text after the frontmatter block.
47
+ */
48
+ function parseFrontmatter(content: string): { frontmatter: Record<string, string | boolean>; body: string } {
49
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
50
+ if (!match) return { frontmatter: {}, body: content };
51
+
52
+ const yaml = match[1];
53
+ const body = match[2] ?? '';
54
+ const frontmatter: Record<string, string | boolean> = {};
55
+
56
+ for (const rawLine of yaml!.split("\n")) {
57
+ const colonIndex = rawLine.indexOf(":");
58
+ if (colonIndex === -1) continue;
59
+ const key = rawLine.slice(0, colonIndex).trim();
60
+ const value = rawLine.slice(colonIndex + 1).trim();
61
+ if (value === "true") {
62
+ frontmatter[key] = true;
63
+ } else if (value === "false") {
64
+ frontmatter[key] = false;
65
+ } else {
66
+ frontmatter[key] = value;
67
+ }
68
+ }
69
+
70
+ return { frontmatter, body };
71
+ }
72
+
73
+ /**
74
+ * Parse command arguments respecting single and double quotes.
75
+ * Handles escape characters with backslash.
76
+ *
77
+ * Examples:
78
+ * 'hello world' → ["hello", "world"]
79
+ * '"hello world"' → ["hello world"]
80
+ * "'hello' \"world\"" → ["hello", "world"]
81
+ */
82
+ function parseCommandArgs(input: string): string[] {
83
+ if (!input.trim()) return [];
84
+
85
+ const args: string[] = [];
86
+ let current = "";
87
+ let inSingle = false;
88
+ let inDouble = false;
89
+ let escape = false;
90
+
91
+ for (const char of input) {
92
+ if (escape) {
93
+ current += char;
94
+ escape = false;
95
+ continue;
96
+ }
97
+ if (char === "\\") {
98
+ escape = true;
99
+ continue;
100
+ }
101
+ if (char === '"' && !inSingle) {
102
+ inDouble = !inDouble;
103
+ continue;
104
+ }
105
+ if (char === "'" && !inDouble) {
106
+ inSingle = !inSingle;
107
+ continue;
108
+ }
109
+ if (char === " " && !inSingle && !inDouble) {
110
+ if (current) {
111
+ args.push(current);
112
+ current = "";
113
+ }
114
+ continue;
115
+ }
116
+ current += char;
117
+ }
118
+ if (current) args.push(current);
119
+ return args;
120
+ }
121
+
122
+ /**
123
+ * Substitute argument placeholders in content.
124
+ *
125
+ * Supports (1-indexed, aligned with bash and Pi's prompt-templates):
126
+ * $1, $2, ... — positional arguments
127
+ * $@ — all arguments joined by space
128
+ * $ARGUMENTS — all arguments joined by space (alias)
129
+ * ${@:N} — arguments from Nth position onwards
130
+ * ${@:N:L} — L arguments starting from position N
131
+ */
132
+ function substituteArgs(content: string, args: string[]): string {
133
+ let result = content;
134
+
135
+ // Replace $1, $2, etc. FIRST (before wildcards to prevent re-substitution)
136
+ result = result.replace(/\$(\d+)/g, (_, num) => {
137
+ const index = parseInt(num, 10) - 1;
138
+ return args[index] ?? "";
139
+ });
140
+
141
+ // Replace ${@:start} or ${@:start:length} (bash-style, 1-indexed)
142
+ result = result.replace(/\$\{@:(\d+)(?::(\d+))?\}/g, (_, startStr, lengthStr) => {
143
+ let start = parseInt(startStr, 10) - 1;
144
+ if (start < 0) start = 0;
145
+ if (lengthStr) {
146
+ const length = parseInt(lengthStr, 10);
147
+ return args.slice(start, start + length).join(" ");
148
+ }
149
+ return args.slice(start).join(" ");
150
+ });
151
+
152
+ const allArgs = args.join(" ");
153
+ result = result.replace(/\$ARGUMENTS/g, allArgs);
154
+ result = result.replace(/\$@/g, allArgs);
155
+
156
+ return result;
157
+ }
158
+
159
+ /**
160
+ * Process shell directives (!`command`) by executing them and inlining stdout.
161
+ * On error: replaces with [Error executing: command].
162
+ * Timeout: DEFAULT_SHELL_TIMEOUT ms (configurable).
163
+ */
164
+ function processShellDirectives(content: string, cwd: string, timeout: number = DEFAULT_SHELL_TIMEOUT): string {
165
+ return content.replace(SHELL_DIRECTIVE_REGEX, (_, command: string) => {
166
+ try {
167
+ return execSync(command, {
168
+ cwd,
169
+ timeout,
170
+ encoding: "utf-8",
171
+ stdio: ["pipe", "pipe", "pipe"],
172
+ }).trim();
173
+ } catch {
174
+ return `[Error executing: ${command}]`;
175
+ }
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Juno Skill Preprocessor Extension
181
+ *
182
+ * Intercepts /skill: commands via the "input" event (before Pi's internal
183
+ * _expandSkillCommand runs), applies variable substitution and shell
184
+ * directive processing, then returns the fully expanded skill block.
185
+ */
186
+ export default function junoSkillPreprocessor(pi: ExtensionAPI) {
187
+ pi.on("input", (event: InputEvent) => {
188
+ const text = typeof event.text === "string" ? event.text : "";
189
+ if (!text.startsWith("/skill:")) return { action: "continue" };
190
+
191
+ const cwd = process.cwd();
192
+
193
+ // Parse skill name and arguments from the command
194
+ const spaceIndex = text.indexOf(" ");
195
+ const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
196
+ const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
197
+ const args = parseCommandArgs(argsString);
198
+
199
+ // Find the skill file on disk
200
+ const skillPath = findSkillFile(skillName, cwd);
201
+ if (!skillPath) return { action: "continue" }; // Unknown skill — let Pi handle it
202
+
203
+ try {
204
+ const content = readFileSync(skillPath, "utf-8");
205
+ const { frontmatter, body } = parseFrontmatter(content);
206
+ let processedBody = body.trim();
207
+
208
+ // Substitute variable placeholders with provided arguments
209
+ if (args.length > 0) {
210
+ processedBody = substituteArgs(processedBody, args);
211
+ }
212
+
213
+ // Execute shell directives (only when explicitly opted in via frontmatter)
214
+ if (frontmatter["enable-shell-directives"] === true) {
215
+ processedBody = processShellDirectives(processedBody, cwd);
216
+ }
217
+
218
+ // Build the <skill> block (matches Pi's _expandSkillCommand format)
219
+ const baseDir = dirname(skillPath);
220
+ const skillBlock = [
221
+ `<skill name="${skillName}" location="${skillPath}">`,
222
+ `References are relative to ${baseDir}.`,
223
+ "",
224
+ processedBody,
225
+ "</skill>",
226
+ ].join("\n");
227
+
228
+ // Return transformed text — Pi's _expandSkillCommand will see this
229
+ // doesn't start with /skill: and pass it through unchanged.
230
+ return { action: "transform", text: skillBlock };
231
+ } catch {
232
+ // On error, let Pi's native expansion handle it
233
+ return { action: "continue" };
234
+ }
235
+ });
236
+ }
237
+
238
+ // Export internals for testing
239
+ export { findSkillFile, parseCommandArgs, parseFrontmatter, processShellDirectives, substituteArgs };
@@ -13,7 +13,7 @@
13
13
  # - If inside venv: installs into venv
14
14
  # - If externally managed Python detected: uses pipx or creates temporary venv
15
15
  # - If outside venv (non-managed): uses --system flag for system-wide installation
16
- # 6. Installs required packages: juno-kanban, roundtable-ai
16
+ # 6. Installs required packages: juno-kanban
17
17
  # 7. Reports if requirements are already satisfied
18
18
  #
19
19
  # Usage: ./install_requirements.sh
@@ -54,13 +54,13 @@ NC='\033[0m' # No Color
54
54
  # Required packages
55
55
  # Note: requests and python-dotenv are required by github.py
56
56
  # slack_sdk is required by Slack integration scripts (slack_fetch.py, slack_respond.py)
57
- REQUIRED_PACKAGES=("juno-kanban" "roundtable-ai" "requests" "python-dotenv" "slack_sdk")
57
+ REQUIRED_PACKAGES=("juno-kanban" "requests" "python-dotenv" "slack_sdk")
58
58
 
59
59
  # Version check cache configuration
60
60
  # This ensures we don't check PyPI on every run (performance optimization per Task RTafs5)
61
61
  VERSION_CHECK_CACHE_DIR="${HOME}/.juno_code"
62
62
  VERSION_CHECK_CACHE_FILE="${VERSION_CHECK_CACHE_DIR}/.version_check_cache"
63
- VERSION_CHECK_INTERVAL_HOURS=24 # Check for updates once per day
63
+ VERSION_CHECK_INTERVAL_HOURS="${VERSION_CHECK_INTERVAL_HOURS:-24}" # Check for updates once per day (override via env var)
64
64
 
65
65
  # Logging functions
66
66
  log_info() {
@@ -402,6 +402,38 @@ is_externally_managed_python() {
402
402
  return 1 # Not externally managed
403
403
  }
404
404
 
405
+ # Function to upgrade pip to latest version inside the active venv
406
+ # Why: venv ships with the pip version bundled in the Python distribution,
407
+ # which can be months/years behind. Old pip may fail to resolve modern
408
+ # dependency metadata or miss security fixes. Upgrading pip is fast (<2s)
409
+ # and prevents hard-to-debug install failures downstream.
410
+ upgrade_pip_in_venv() {
411
+ if ! is_in_virtualenv; then
412
+ return 0 # Only upgrade pip inside a venv
413
+ fi
414
+
415
+ log_info "Upgrading pip to latest version in venv..."
416
+
417
+ # Prefer uv for speed, fall back to pip itself
418
+ if command -v uv &>/dev/null; then
419
+ if uv pip install --upgrade pip --quiet 2>/dev/null; then
420
+ local pip_ver
421
+ pip_ver=$(python3 -m pip --version 2>/dev/null | awk '{print $2}' || echo "unknown")
422
+ log_success "pip upgraded to v$pip_ver (via uv)"
423
+ return 0
424
+ fi
425
+ fi
426
+
427
+ # Fall back to pip self-upgrade
428
+ if python3 -m pip install --upgrade pip --quiet 2>/dev/null; then
429
+ local pip_ver
430
+ pip_ver=$(python3 -m pip --version 2>/dev/null | awk '{print $2}' || echo "unknown")
431
+ log_success "pip upgraded to v$pip_ver"
432
+ else
433
+ log_warning "Could not upgrade pip (non-fatal, continuing with current version)"
434
+ fi
435
+ }
436
+
405
437
  # Function to install packages using pipx
406
438
  install_with_pipx() {
407
439
  log_info "Installing packages using 'pipx' (recommended for Python applications)..."
@@ -492,6 +524,9 @@ install_with_uv() {
492
524
  log_error "Virtual environment activation script not found"
493
525
  return 1
494
526
  fi
527
+
528
+ # Upgrade pip to latest version in venv
529
+ upgrade_pip_in_venv
495
530
  fi
496
531
 
497
532
  local failed_packages=()
@@ -562,6 +597,9 @@ install_with_pip() {
562
597
  source "$venv_path/bin/activate"
563
598
  log_success "Activated virtual environment"
564
599
  python_cmd="python" # Use the venv's python
600
+
601
+ # Upgrade pip to latest version in venv
602
+ upgrade_pip_in_venv
565
603
  fi
566
604
 
567
605
  local failed_packages=()
@@ -170,12 +170,30 @@ ensure_python_environment() {
170
170
  # Get the directory where this script is located
171
171
  SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
172
172
 
173
- # Navigate to project root (parent of scripts directory)
174
- PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd )"
173
+ # Auto-discover project root by walking up the directory tree looking for .juno_task/
174
+ # This makes kanban.sh work from any installation depth (e.g. .juno_task/scripts/,
175
+ # .claude/skills/ralph-loop/scripts/, etc.) without a hardcoded relative path.
176
+ PROJECT_ROOT=""
177
+ _dir="$SCRIPT_DIR"
178
+ while [[ "$_dir" != "/" ]]; do
179
+ if [[ -d "$_dir/.juno_task" ]]; then
180
+ PROJECT_ROOT="$_dir"
181
+ break
182
+ fi
183
+ _dir="$( cd "$_dir/.." && pwd )"
184
+ done
185
+ if [[ -z "$PROJECT_ROOT" ]]; then
186
+ echo "ERROR: Could not find project root (no .juno_task/ directory found above $SCRIPT_DIR)" >&2
187
+ exit 1
188
+ fi
175
189
 
176
190
  # Change to project root
177
191
  cd "$PROJECT_ROOT"
178
192
 
193
+ # Export JUNO_TASK_ROOT so juno-kanban resolves .juno_task paths from project root,
194
+ # not from wherever the calling agent happens to be. Respects existing override.
195
+ export JUNO_TASK_ROOT="${JUNO_TASK_ROOT:-$PROJECT_ROOT}"
196
+
179
197
  # Arrays to store normalized arguments (declared at script level for proper handling)
180
198
  declare -a NORMALIZED_GLOBAL_FLAGS=()
181
199
  declare -a NORMALIZED_COMMAND_ARGS=()
@@ -192,7 +210,7 @@ normalize_arguments() {
192
210
  local found_command=false
193
211
 
194
212
  # Known subcommands
195
- local commands="create search get show update archive mark list merge"
213
+ local commands="create search get show update archive mark list merge ready deps order"
196
214
 
197
215
  while [[ $# -gt 0 ]]; do
198
216
  case $1 in
@@ -207,7 +225,7 @@ normalize_arguments() {
207
225
  fi
208
226
  ;;
209
227
  # Global flags that don't take a value
210
- -p|--pretty|--raw|-v|--verbose|-h|--help|--version)
228
+ -p|--pretty|--raw|-v|--verbose|--version)
211
229
  NORMALIZED_GLOBAL_FLAGS+=("$1")
212
230
  shift
213
231
  ;;