@soleri/cli 9.3.0 → 9.4.0

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.
@@ -1,21 +1,31 @@
1
- #!/usr/bin/env bash
1
+ #!/bin/sh
2
2
  # Anti-Deletion Staging Hook for Claude Code (Soleri Hook Pack: yolo-safety)
3
- # PreToolUse -> Bash: intercepts rm, rmdir, mv (of project dirs), git clean, reset --hard
4
- # Copies target files to ~/.soleri/staging/<timestamp>/ then blocks the command.
3
+ # PreToolUse -> Bash: intercepts destructive commands, stages files, blocks execution.
4
+ #
5
+ # Intercepted patterns:
6
+ # - rm / rmdir (files/dirs — stages first, then blocks)
7
+ # - git push --force (blocks outright)
8
+ # - git reset --hard (blocks outright)
9
+ # - git clean (blocks outright)
10
+ # - git checkout -- . (blocks outright)
11
+ # - git restore . (blocks outright)
12
+ # - mv ~/projects/... (blocks outright)
13
+ # - drop table (SQL — blocks outright)
14
+ # - docker rm / rmi (blocks outright)
5
15
  #
6
16
  # Catastrophic commands (rm -rf /, rm -rf ~) should stay in deny rules —
7
17
  # this hook handles targeted deletes only.
8
18
  #
9
- # Dependencies: jq (required), perl (optional, for heredoc stripping)
19
+ # Dependencies: jq (required)
20
+ # POSIX sh compatible — no bash-specific features.
10
21
 
11
- set -euo pipefail
22
+ set -eu
12
23
 
13
24
  STAGING_ROOT="$HOME/.soleri/staging"
14
- PROJECTS_DIR="$HOME/projects"
15
25
  INPUT=$(cat)
16
26
 
17
27
  # Extract the command from stdin JSON
18
- CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
28
+ CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
19
29
 
20
30
  # No command found — let it through
21
31
  if [ -z "$CMD" ]; then
@@ -26,12 +36,17 @@ fi
26
36
  # Commands like: gh issue comment --body "$(cat <<'EOF' ... rmdir ... EOF)"
27
37
  # contain destructive keywords in text, not as actual commands.
28
38
 
29
- # Remove heredoc blocks: <<'EOF'...EOF and <<EOF...EOF (multiline)
30
- STRIPPED=$(echo "$CMD" | perl -0777 -pe "s/<<'?\\w+'?.*?^\\w+$//gms" 2>/dev/null || echo "$CMD")
31
- # Remove double-quoted strings (greedy but good enough for this check)
32
- STRIPPED=$(echo "$STRIPPED" | sed -E 's/"[^"]*"//g' 2>/dev/null || echo "$STRIPPED")
39
+ # Remove heredoc blocks (best-effort with sed)
40
+ STRIPPED=$(printf '%s' "$CMD" | sed -e "s/<<'[A-Za-z_]*'.*//g" -e 's/<<[A-Za-z_]*.*//g' 2>/dev/null || printf '%s' "$CMD")
41
+ # Remove double-quoted strings
42
+ STRIPPED=$(printf '%s' "$STRIPPED" | sed 's/"[^"]*"//g' 2>/dev/null || printf '%s' "$STRIPPED")
33
43
  # Remove single-quoted strings
34
- STRIPPED=$(echo "$STRIPPED" | sed -E "s/'[^']*'//g" 2>/dev/null || echo "$STRIPPED")
44
+ STRIPPED=$(printf '%s' "$STRIPPED" | sed "s/'[^']*'//g" 2>/dev/null || printf '%s' "$STRIPPED")
45
+
46
+ # --- Helper: check if pattern matches stripped command ---
47
+ matches() {
48
+ printf '%s' "$STRIPPED" | grep -qE "$1"
49
+ }
35
50
 
36
51
  # --- Detect destructive commands (on stripped command only) ---
37
52
 
@@ -42,56 +57,78 @@ IS_GIT_CLEAN=false
42
57
  IS_RESET_HARD=false
43
58
  IS_GIT_CHECKOUT_DOT=false
44
59
  IS_GIT_RESTORE_DOT=false
60
+ IS_GIT_PUSH_FORCE=false
61
+ IS_DROP_TABLE=false
62
+ IS_DOCKER_RM=false
45
63
 
46
- # Check for rm commands (but not git rm which is safe — it stages, doesn't destroy)
47
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)rm\s'; then
48
- if ! echo "$STRIPPED" | grep -qE '(^|\s)git\s+rm\s'; then
64
+ # rm (but not git rm which stages, doesn't destroy)
65
+ if matches '(^|\s|;|&&|\|\|)rm\s'; then
66
+ if ! matches '(^|\s)git\s+rm\s'; then
49
67
  IS_RM=true
50
68
  fi
51
69
  fi
52
70
 
53
- # Check for rmdir commands
54
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)rmdir\s'; then
71
+ # rmdir
72
+ if matches '(^|\s|;|&&|\|\|)rmdir\s'; then
55
73
  IS_RMDIR=true
56
74
  fi
57
75
 
58
- # Check for mv commands that move project directories or git repos
59
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)mv\s'; then
60
- MV_SOURCES=$(echo "$STRIPPED" | sed -E 's/^.*\bmv\s+//' | sed -E 's/-(f|i|n|v)\s+//g')
61
- if echo "$MV_SOURCES" | grep -qE "(~/projects|$HOME/projects|\\\$HOME/projects|\\.git)"; then
76
+ # mv of project directories or git repos
77
+ if matches '(^|\s|;|&&|\|\|)mv\s'; then
78
+ MV_TAIL=$(printf '%s' "$STRIPPED" | sed 's/^.*\bmv //' | sed 's/-[finv] //g')
79
+ if printf '%s' "$MV_TAIL" | grep -qE '(~/projects|\.git)'; then
62
80
  IS_MV_PROJECT=true
63
81
  fi
64
82
  fi
65
83
 
66
- # Check for git clean
67
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+clean\b'; then
84
+ # git clean
85
+ if matches '(^|\s|;|&&|\|\|)git\s+clean\b'; then
68
86
  IS_GIT_CLEAN=true
69
87
  fi
70
88
 
71
- # Check for git reset --hard
72
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+reset\s+--hard'; then
89
+ # git reset --hard
90
+ if matches '(^|\s|;|&&|\|\|)git\s+reset\s+--hard'; then
73
91
  IS_RESET_HARD=true
74
92
  fi
75
93
 
76
- # Check for git checkout -- . (restores all files, discards changes)
77
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+checkout\s+--\s+\.'; then
94
+ # git checkout -- .
95
+ if matches '(^|\s|;|&&|\|\|)git\s+checkout\s+--\s+\.'; then
78
96
  IS_GIT_CHECKOUT_DOT=true
79
97
  fi
80
98
 
81
- # Check for git restore . (restores all files, discards changes)
82
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+restore\s+\.'; then
99
+ # git restore .
100
+ if matches '(^|\s|;|&&|\|\|)git\s+restore\s+\.'; then
83
101
  IS_GIT_RESTORE_DOT=true
84
102
  fi
85
103
 
86
- # Not a destructive command let it through
104
+ # git push --force / -f (but not --force-with-lease which is safer)
105
+ if matches '(^|\s|;|&&|\|\|)git\s+push\s'; then
106
+ if matches 'git\s+push\s.*--force([^-]|$)' || matches 'git\s+push\s+-f(\s|$)' || matches 'git\s+push\s.*\s-f(\s|$)'; then
107
+ IS_GIT_PUSH_FORCE=true
108
+ fi
109
+ fi
110
+
111
+ # SQL drop table (case-insensitive)
112
+ if printf '%s' "$STRIPPED" | grep -qiE '(^|\s|;)drop\s+table'; then
113
+ IS_DROP_TABLE=true
114
+ fi
115
+
116
+ # docker rm / docker rmi
117
+ if matches '(^|\s|;|&&|\|\|)docker\s+(rm|rmi)\b'; then
118
+ IS_DOCKER_RM=true
119
+ fi
120
+
121
+ # --- Not a destructive command — let it through ---
122
+
87
123
  if [ "$IS_RM" = false ] && [ "$IS_RMDIR" = false ] && [ "$IS_MV_PROJECT" = false ] && \
88
124
  [ "$IS_GIT_CLEAN" = false ] && [ "$IS_RESET_HARD" = false ] && \
89
- [ "$IS_GIT_CHECKOUT_DOT" = false ] && [ "$IS_GIT_RESTORE_DOT" = false ]; then
125
+ [ "$IS_GIT_CHECKOUT_DOT" = false ] && [ "$IS_GIT_RESTORE_DOT" = false ] && \
126
+ [ "$IS_GIT_PUSH_FORCE" = false ] && [ "$IS_DROP_TABLE" = false ] && \
127
+ [ "$IS_DOCKER_RM" = false ]; then
90
128
  exit 0
91
129
  fi
92
130
 
93
- # --- Handle git clean (block outright) ---
94
-
131
+ # --- Block: git clean ---
95
132
  if [ "$IS_GIT_CLEAN" = true ]; then
96
133
  jq -n '{
97
134
  continue: false,
@@ -100,8 +137,7 @@ if [ "$IS_GIT_CLEAN" = true ]; then
100
137
  exit 0
101
138
  fi
102
139
 
103
- # --- Handle git reset --hard (block outright) ---
104
-
140
+ # --- Block: git reset --hard ---
105
141
  if [ "$IS_RESET_HARD" = true ]; then
106
142
  jq -n '{
107
143
  continue: false,
@@ -110,8 +146,7 @@ if [ "$IS_RESET_HARD" = true ]; then
110
146
  exit 0
111
147
  fi
112
148
 
113
- # --- Handle git checkout -- . (block outright) ---
114
-
149
+ # --- Block: git checkout -- . ---
115
150
  if [ "$IS_GIT_CHECKOUT_DOT" = true ]; then
116
151
  jq -n '{
117
152
  continue: false,
@@ -120,8 +155,7 @@ if [ "$IS_GIT_CHECKOUT_DOT" = true ]; then
120
155
  exit 0
121
156
  fi
122
157
 
123
- # --- Handle git restore . (block outright) ---
124
-
158
+ # --- Block: git restore . ---
125
159
  if [ "$IS_GIT_RESTORE_DOT" = true ]; then
126
160
  jq -n '{
127
161
  continue: false,
@@ -130,8 +164,16 @@ if [ "$IS_GIT_RESTORE_DOT" = true ]; then
130
164
  exit 0
131
165
  fi
132
166
 
133
- # --- Handle mv of project directories (block outright) ---
167
+ # --- Block: git push --force ---
168
+ if [ "$IS_GIT_PUSH_FORCE" = true ]; then
169
+ jq -n '{
170
+ continue: false,
171
+ stopReason: "BLOCKED: git push --force can overwrite remote history and cause data loss for collaborators. Use --force-with-lease instead, or ask the user to run this manually."
172
+ }'
173
+ exit 0
174
+ fi
134
175
 
176
+ # --- Block: mv of project directories ---
135
177
  if [ "$IS_MV_PROJECT" = true ]; then
136
178
  jq -n '{
137
179
  continue: false,
@@ -140,8 +182,7 @@ if [ "$IS_MV_PROJECT" = true ]; then
140
182
  exit 0
141
183
  fi
142
184
 
143
- # --- Handle rmdir (block outright) ---
144
-
185
+ # --- Block: rmdir ---
145
186
  if [ "$IS_RMDIR" = true ]; then
146
187
  jq -n '{
147
188
  continue: false,
@@ -150,6 +191,24 @@ if [ "$IS_RMDIR" = true ]; then
150
191
  exit 0
151
192
  fi
152
193
 
194
+ # --- Block: drop table ---
195
+ if [ "$IS_DROP_TABLE" = true ]; then
196
+ jq -n '{
197
+ continue: false,
198
+ stopReason: "BLOCKED: DROP TABLE detected. This would permanently destroy database data. Ask the user to run this SQL statement manually after confirming intent."
199
+ }'
200
+ exit 0
201
+ fi
202
+
203
+ # --- Block: docker rm / rmi ---
204
+ if [ "$IS_DOCKER_RM" = true ]; then
205
+ jq -n '{
206
+ continue: false,
207
+ stopReason: "BLOCKED: docker rm/rmi detected. Removing containers or images can cause data loss. Ask the user to run this manually."
208
+ }'
209
+ exit 0
210
+ fi
211
+
153
212
  # --- Handle rm commands — copy to staging, then block ---
154
213
 
155
214
  # Create timestamped staging directory
@@ -158,7 +217,7 @@ STAGE_DIR="$STAGING_ROOT/$TIMESTAMP"
158
217
 
159
218
  # Extract file paths from the rm command
160
219
  # Strip rm and its flags, keeping only the file arguments
161
- FILES=$(echo "$CMD" | sed -E 's/^.*\brm\s+//' | sed -E 's/-(r|f|rf|fr|v|i|rv|fv|rfv|frv)\s+//g' | tr ' ' '\n' | grep -v '^-' | grep -v '^$')
220
+ FILES=$(printf '%s' "$CMD" | sed 's/^.*\brm //' | sed 's/-[rRfivd]* //g' | tr ' ' '\n' | grep -v '^-' | grep -v '^$' || true)
162
221
 
163
222
  if [ -z "$FILES" ]; then
164
223
  jq -n '{
@@ -168,14 +227,15 @@ if [ -z "$FILES" ]; then
168
227
  exit 0
169
228
  fi
170
229
 
171
- STAGED=()
172
- MISSING=()
230
+ STAGED_COUNT=0
231
+ STAGED_LIST=""
232
+ MISSING_COUNT=0
173
233
 
174
234
  mkdir -p "$STAGE_DIR"
175
235
 
176
- while IFS= read -r filepath; do
236
+ printf '%s\n' "$FILES" | while IFS= read -r filepath; do
177
237
  # Expand path (handle ~, relative paths)
178
- expanded=$(eval echo "$filepath" 2>/dev/null || echo "$filepath")
238
+ expanded=$(eval printf '%s' "$filepath" 2>/dev/null || printf '%s' "$filepath")
179
239
 
180
240
  if [ -e "$expanded" ]; then
181
241
  # Preserve directory structure in staging
@@ -183,32 +243,32 @@ while IFS= read -r filepath; do
183
243
  mkdir -p "$target_dir"
184
244
  # COPY instead of MOVE — originals stay intact, staging is a backup
185
245
  if [ -d "$expanded" ]; then
186
- cp -R "$expanded" "$target_dir/" 2>/dev/null && STAGED+=("$expanded") || MISSING+=("$expanded")
246
+ # Use rsync if available (excludes node_modules/dist/.git), fall back to cp
247
+ if command -v rsync >/dev/null 2>&1; then
248
+ rsync -a --exclude='node_modules' --exclude='dist' --exclude='.git' "$expanded/" "$target_dir/$(basename "$expanded")/" 2>/dev/null
249
+ else
250
+ cp -R "$expanded" "$target_dir/" 2>/dev/null
251
+ fi
187
252
  else
188
- cp "$expanded" "$target_dir/" 2>/dev/null && STAGED+=("$expanded") || MISSING+=("$expanded")
253
+ cp "$expanded" "$target_dir/" 2>/dev/null
189
254
  fi
190
- else
191
- MISSING+=("$expanded")
192
255
  fi
193
- done <<< "$FILES"
256
+ done
194
257
 
195
- # Build response
196
- STAGED_COUNT=${#STAGED[@]}
197
- MISSING_COUNT=${#MISSING[@]}
258
+ # Count what was staged (check if staging dir has content)
259
+ if [ -d "$STAGE_DIR" ] && [ "$(ls -A "$STAGE_DIR" 2>/dev/null)" ]; then
260
+ STAGED_COUNT=$(find "$STAGE_DIR" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')
261
+ fi
198
262
 
199
- if [ "$STAGED_COUNT" -eq 0 ] && [ "$MISSING_COUNT" -gt 0 ]; then
263
+ if [ "$STAGED_COUNT" -eq 0 ]; then
200
264
  # All files were missing — let the rm fail naturally
201
265
  rmdir "$STAGE_DIR" 2>/dev/null || true
202
266
  exit 0
203
267
  fi
204
268
 
205
- STAGED_LIST=$(printf '%s, ' "${STAGED[@]}" | sed 's/, $//')
206
-
207
269
  jq -n \
208
- --arg staged "$STAGED_LIST" \
209
270
  --arg dir "$STAGE_DIR" \
210
- --argjson count "$STAGED_COUNT" \
211
271
  '{
212
272
  continue: false,
213
- stopReason: ("BLOCKED & BACKED UP: " + ($count | tostring) + " item(s) copied to " + $dir + " — files: " + $staged + ". The originals are untouched. To proceed with deletion, ask the user to run the rm command manually.")
273
+ stopReason: ("BLOCKED & BACKED UP: Files copied to " + $dir + ". The originals are untouched. To proceed with deletion, ask the user to run the rm command manually.")
214
274
  }'
package/dist/main.js CHANGED
@@ -20,6 +20,7 @@ import { registerSkills } from './commands/skills.js';
20
20
  import { registerAgent } from './commands/agent.js';
21
21
  import { registerTelegram } from './commands/telegram.js';
22
22
  import { registerStaging } from './commands/staging.js';
23
+ import { registerYolo } from './commands/yolo.js';
23
24
  const require = createRequire(import.meta.url);
24
25
  const { version } = require('../package.json');
25
26
  const RESET = '\x1b[0m';
@@ -75,5 +76,6 @@ registerSkills(program);
75
76
  registerAgent(program);
76
77
  registerTelegram(program);
77
78
  registerStaging(program);
79
+ registerYolo(program);
78
80
  program.parse();
79
81
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soleri/cli",
3
- "version": "9.3.0",
3
+ "version": "9.4.0",
4
4
  "description": "Developer CLI for creating and managing Soleri AI agents.",
5
5
  "keywords": [
6
6
  "agent",
@@ -162,7 +162,7 @@ describe('hook-packs', () => {
162
162
  const settings = JSON.parse(readFileSync(join(claudeDir, 'settings.json'), 'utf-8'));
163
163
  expect(settings.hooks).toBeDefined();
164
164
  expect(settings.hooks.PreToolUse).toHaveLength(1);
165
- expect(settings.hooks.PreToolUse[0].command).toBe('bash ~/.claude/hooks/anti-deletion.sh');
165
+ expect(settings.hooks.PreToolUse[0].command).toBe('sh ~/.claude/hooks/anti-deletion.sh');
166
166
  expect(settings.hooks.PreToolUse[0]._soleriPack).toBe('yolo-safety');
167
167
  });
168
168
 
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Comprehensive E2E tests for the interactive create wizard.
4
4
  *
5
- * Tests all 7 archetypes, custom path, custom greeting, custom domains/principles,
5
+ * Tests custom path, custom greeting, custom domains/principles,
6
6
  * hook pack selection, cancel flows, and decline flows.
7
7
  *
8
8
  * Run: node packages/cli/src/__tests__/wizard-e2e.mjs
@@ -26,6 +26,7 @@ import {
26
26
  generateInjectClaudeMd,
27
27
  generateSkills,
28
28
  } from '@soleri/forge/lib';
29
+ import { composeClaudeMd, getEngineRulesContent } from '@soleri/forge/lib';
29
30
  import type { AgentConfig } from '@soleri/forge/lib';
30
31
  import { detectAgent } from '../utils/agent-context.js';
31
32
  import { installClaude } from './install.js';
@@ -194,7 +195,70 @@ export function registerAgent(program: Command): void {
194
195
  return;
195
196
  }
196
197
 
197
- // Reconstruct AgentConfig from the existing agent
198
+ // ─── File-tree agent (v7+) ────────────────────────────────
199
+ if (ctx.format === 'filetree') {
200
+ const enginePath = join(ctx.agentPath, 'instructions', '_engine.md');
201
+ const claudeMdPath = join(ctx.agentPath, 'CLAUDE.md');
202
+
203
+ // Generate skills from latest forge templates
204
+ const skillFiles = opts.skipSkills
205
+ ? []
206
+ : generateSkills({ id: ctx.agentId } as AgentConfig);
207
+
208
+ if (opts.dryRun) {
209
+ p.log.info(`Would regenerate: ${enginePath}`);
210
+ p.log.info(`Would regenerate: ${claudeMdPath}`);
211
+ if (skillFiles.length > 0) {
212
+ const newSkills = skillFiles.filter(
213
+ ([relPath]) => !existsSync(join(ctx.agentPath, relPath)),
214
+ );
215
+ const updatedSkills = skillFiles.filter(([relPath]) =>
216
+ existsSync(join(ctx.agentPath, relPath)),
217
+ );
218
+ p.log.info(
219
+ `Skills: ${skillFiles.length} total (${newSkills.length} new, ${updatedSkills.length} updated)`,
220
+ );
221
+ for (const [relPath] of newSkills) {
222
+ p.log.info(` + ${relPath}`);
223
+ }
224
+ }
225
+ p.log.info(`Agent: ${ctx.agentId} (file-tree format)`);
226
+ return;
227
+ }
228
+
229
+ // 1. Sync skills from forge templates
230
+ if (skillFiles.length > 0) {
231
+ let newCount = 0;
232
+ let updatedCount = 0;
233
+ for (const [relPath, content] of skillFiles) {
234
+ const fullPath = join(ctx.agentPath, relPath);
235
+ const dirPath = dirname(fullPath);
236
+ const isNew = !existsSync(fullPath);
237
+ mkdirSync(dirPath, { recursive: true });
238
+ writeFileSync(fullPath, content, 'utf-8');
239
+ if (isNew) newCount++;
240
+ else updatedCount++;
241
+ }
242
+ p.log.success(
243
+ `Synced ${skillFiles.length} skills (${newCount} new, ${updatedCount} updated)`,
244
+ );
245
+ }
246
+
247
+ // 2. Regenerate _engine.md from latest shared-rules
248
+ mkdirSync(join(ctx.agentPath, 'instructions'), { recursive: true });
249
+ writeFileSync(enginePath, getEngineRulesContent(), 'utf-8');
250
+ p.log.success(`Regenerated ${enginePath}`);
251
+
252
+ // 3. Recompose CLAUDE.md from agent.yaml + instructions + workflows + skills
253
+ const result = composeClaudeMd(ctx.agentPath);
254
+ writeFileSync(claudeMdPath, result.content, 'utf-8');
255
+ p.log.success(
256
+ `Regenerated ${claudeMdPath} (${result.sources.length} sources, ${result.content.length} bytes)`,
257
+ );
258
+ return;
259
+ }
260
+
261
+ // ─── Legacy TypeScript agent ──────────────────────────────
198
262
  const config = readAgentConfig(ctx.agentPath, ctx.agentId);
199
263
  if (!config) {
200
264
  p.log.error('Could not read agent config from persona.ts and entry point.');
@@ -216,7 +280,6 @@ export function registerAgent(program: Command): void {
216
280
  p.log.info(`Agent: ${config.name} (${config.domains.length} domains)`);
217
281
  p.log.info(`Domains: ${config.domains.join(', ')}`);
218
282
  if (skillFiles.length > 0) {
219
- // Check which skills are new vs existing
220
283
  const newSkills = skillFiles.filter(
221
284
  ([relPath]) => !existsSync(join(ctx.agentPath, relPath)),
222
285
  );
@@ -13,6 +13,18 @@ import type { Command } from 'commander';
13
13
  import * as p from '@clack/prompts';
14
14
  import { PackLockfile, inferPackType, resolvePack, checkNpmVersion } from '@soleri/core';
15
15
  import type { LockEntry, PackSource } from '@soleri/core';
16
+
17
+ // ─── Tier display helpers ────────────────────────────────────────────
18
+
19
+ const TIER_BADGES: Record<string, string> = {
20
+ default: '[default]',
21
+ community: '[community]',
22
+ premium: '[premium]',
23
+ };
24
+
25
+ function tierBadge(tier?: string): string {
26
+ return TIER_BADGES[tier ?? 'community'] ?? '[community]';
27
+ }
16
28
  import { detectAgent } from '../utils/agent-context.js';
17
29
 
18
30
  const LOCKFILE_NAME = 'soleri.lock';
@@ -45,14 +57,18 @@ export function registerPack(program: Command): void {
45
57
  pack
46
58
  .command('list')
47
59
  .option('--type <type>', 'Filter by pack type (hooks, skills, knowledge, domain, bundle)')
60
+ .option('--tier <tier>', 'Filter by tier (default, community, premium)')
48
61
  .description('List installed packs')
49
- .action((opts: { type?: string }) => {
62
+ .action((opts: { type?: string; tier?: string }) => {
50
63
  const lockfile = new PackLockfile(getLockfilePath());
51
64
  let entries = lockfile.list();
52
65
 
53
66
  if (opts.type) {
54
67
  entries = entries.filter((e) => e.type === opts.type);
55
68
  }
69
+ if (opts.tier) {
70
+ entries = entries.filter((e) => (e.tier ?? 'community') === opts.tier);
71
+ }
56
72
 
57
73
  if (entries.length === 0) {
58
74
  p.log.info('No packs installed.');
@@ -61,9 +77,10 @@ export function registerPack(program: Command): void {
61
77
 
62
78
  p.log.info(`${entries.length} pack(s) installed:\n`);
63
79
  for (const entry of entries) {
64
- const badge =
80
+ const source =
65
81
  entry.source === 'built-in' ? ' [built-in]' : entry.source === 'npm' ? ' [npm]' : '';
66
- console.log(` ${entry.id}@${entry.version} ${entry.type}${badge}`);
82
+ const tier = ` ${tierBadge(entry.tier)}`;
83
+ console.log(` ${entry.id}@${entry.version} ${entry.type}${tier}${source}`);
67
84
  if (entry.vaultEntries > 0) console.log(` vault: ${entry.vaultEntries} entries`);
68
85
  if (entry.skills.length > 0) console.log(` skills: ${entry.skills.join(', ')}`);
69
86
  if (entry.hooks.length > 0) console.log(` hooks: ${entry.hooks.join(', ')}`);
@@ -163,6 +180,7 @@ export function registerPack(program: Command): void {
163
180
  skills,
164
181
  hooks,
165
182
  facadesRegistered: (manifest.facades?.length ?? 0) > 0,
183
+ tier: manifest.tier ?? 'community',
166
184
  };
167
185
 
168
186
  lockfile.set(entry);
@@ -219,9 +237,14 @@ export function registerPack(program: Command): void {
219
237
  return;
220
238
  }
221
239
 
240
+ const tierLabel = entry.tier ?? 'community';
241
+ const tierNote =
242
+ tierLabel === 'premium' ? ' (currently unlocked — premium platform coming soon)' : '';
243
+
222
244
  console.log(`\n Pack: ${entry.id}`);
223
245
  console.log(` Version: ${entry.version}`);
224
246
  console.log(` Type: ${entry.type}`);
247
+ console.log(` Tier: ${tierLabel}${tierNote}`);
225
248
  console.log(` Source: ${entry.source}`);
226
249
  console.log(` Directory: ${entry.directory}`);
227
250
  console.log(` Installed: ${entry.installedAt}`);
@@ -378,7 +401,16 @@ export function registerPack(program: Command): void {
378
401
  return;
379
402
  }
380
403
 
381
- let total = 0;
404
+ // Collect all packs with their tier
405
+ const allPacks: Array<{
406
+ id: string;
407
+ version: string;
408
+ description: string;
409
+ domains: string;
410
+ tier: string;
411
+ category: string;
412
+ }> = [];
413
+
382
414
  for (const baseDir of searchDirs) {
383
415
  const categories = readdirSync(baseDir, { withFileTypes: true })
384
416
  .filter((d) => d.isDirectory())
@@ -390,18 +422,19 @@ export function registerPack(program: Command): void {
390
422
  (d) => d.isDirectory() && existsSync(join(categoryDir, d.name, 'soleri-pack.json')),
391
423
  );
392
424
 
393
- if (packs.length === 0) continue;
394
-
395
- console.log(`\n ${category}/`);
396
425
  for (const pk of packs) {
397
426
  try {
398
427
  const manifest = JSON.parse(
399
428
  readFileSync(join(categoryDir, pk.name, 'soleri-pack.json'), 'utf-8'),
400
429
  );
401
- const domains = (manifest.domains as string[])?.join(', ') || '—';
402
- console.log(` ${manifest.id}@${manifest.version} ${manifest.description || ''}`);
403
- console.log(` domains: ${domains}`);
404
- total++;
430
+ allPacks.push({
431
+ id: manifest.id,
432
+ version: manifest.version,
433
+ description: manifest.description || '',
434
+ domains: (manifest.domains as string[])?.join(', ') || '—',
435
+ tier: manifest.tier ?? 'community',
436
+ category,
437
+ });
405
438
  } catch {
406
439
  // skip malformed packs
407
440
  }
@@ -409,11 +442,31 @@ export function registerPack(program: Command): void {
409
442
  }
410
443
  }
411
444
 
412
- if (total === 0) {
445
+ if (allPacks.length === 0) {
413
446
  p.log.info('No packs found.');
414
- } else {
415
- console.log(`\n ${total} pack(s) available.\n`);
447
+ return;
416
448
  }
449
+
450
+ // Group by tier and display
451
+ const tierOrder: Array<{ key: string; label: string }> = [
452
+ { key: 'default', label: 'Default (included with Soleri)' },
453
+ { key: 'community', label: 'Community (free)' },
454
+ { key: 'premium', label: 'Premium (included — premium platform coming soon)' },
455
+ ];
456
+
457
+ for (const { key, label } of tierOrder) {
458
+ const tierPacks = allPacks.filter((pk) => pk.tier === key);
459
+ if (tierPacks.length === 0) continue;
460
+
461
+ console.log(`\n ${label}`);
462
+ console.log(` ${'─'.repeat(label.length)}`);
463
+ for (const pk of tierPacks) {
464
+ console.log(` ${pk.id}@${pk.version} ${pk.description}`);
465
+ console.log(` domains: ${pk.domains}`);
466
+ }
467
+ }
468
+
469
+ console.log(`\n ${allPacks.length} pack(s) available.\n`);
417
470
  });
418
471
 
419
472
  // ─── create ─────────────────────────────────────────────────
@@ -441,6 +494,18 @@ export function registerPack(program: Command): void {
441
494
  });
442
495
  if (p.isCancel(description)) return;
443
496
 
497
+ const tier = await p.select({
498
+ message: 'Pack tier:',
499
+ options: [
500
+ { value: 'community', label: 'Community — free, published to npm' },
501
+ {
502
+ value: 'premium',
503
+ label: 'Premium — requires Soleri platform account (coming soon)',
504
+ },
505
+ ],
506
+ });
507
+ if (p.isCancel(tier)) return;
508
+
444
509
  const author = await p.text({ message: 'Author:', placeholder: '@username' });
445
510
  if (p.isCancel(author)) return;
446
511
 
@@ -454,6 +519,7 @@ export function registerPack(program: Command): void {
454
519
  id: name,
455
520
  version: '1.0.0',
456
521
  description: description || '',
522
+ tier: tier || 'community',
457
523
  author: author || '',
458
524
  license: 'MIT',
459
525
  soleri: '>=2.0.0',