@cnrai/pave 0.3.32 → 0.3.34

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 (83) hide show
  1. package/MARKETPLACE.md +406 -0
  2. package/README.md +218 -21
  3. package/build-binary.js +591 -0
  4. package/build-npm.js +537 -0
  5. package/build.js +230 -0
  6. package/check-binary.js +26 -0
  7. package/deploy.sh +95 -0
  8. package/index.js +5775 -0
  9. package/lib/agent-registry.js +1037 -0
  10. package/lib/args-parser.js +837 -0
  11. package/lib/blessed-widget-patched.js +93 -0
  12. package/lib/cli-markdown.js +590 -0
  13. package/lib/compaction.js +153 -0
  14. package/lib/duration.js +94 -0
  15. package/lib/hash.js +22 -0
  16. package/lib/marketplace.js +866 -0
  17. package/lib/memory-config.js +166 -0
  18. package/lib/skill-manager.js +891 -0
  19. package/lib/soul.js +31 -0
  20. package/lib/tool-output-formatter.js +180 -0
  21. package/package.json +35 -33
  22. package/start-pave.sh +149 -0
  23. package/status.js +271 -0
  24. package/test/abort-stream.test.js +445 -0
  25. package/test/agent-auto-compaction.test.js +552 -0
  26. package/test/agent-comm-abort.test.js +95 -0
  27. package/test/agent-comm.test.js +598 -0
  28. package/test/agent-inbox.test.js +576 -0
  29. package/test/agent-init.test.js +264 -0
  30. package/test/agent-interrupt.test.js +314 -0
  31. package/test/agent-lifecycle.test.js +520 -0
  32. package/test/agent-log-files.test.js +349 -0
  33. package/test/agent-mode.manual-test.js +392 -0
  34. package/test/agent-parsing.test.js +228 -0
  35. package/test/agent-post-stream-idle.test.js +762 -0
  36. package/test/agent-registry.test.js +359 -0
  37. package/test/agent-rm.test.js +442 -0
  38. package/test/agent-spawn.test.js +933 -0
  39. package/test/agent-status-api.test.js +624 -0
  40. package/test/agent-update.test.js +435 -0
  41. package/test/args-parser.test.js +391 -0
  42. package/test/auto-compaction-chat.manual-test.js +227 -0
  43. package/test/auto-compaction.test.js +941 -0
  44. package/test/build-config.test.js +120 -0
  45. package/test/build-npm.test.js +388 -0
  46. package/test/chat-command.test.js +137 -0
  47. package/test/chat-leading-lines.test.js +159 -0
  48. package/test/config-flag.test.js +272 -0
  49. package/test/cursor-drift.test.js +135 -0
  50. package/test/debug-require.js +23 -0
  51. package/test/dir-migration.test.js +323 -0
  52. package/test/duration.test.js +229 -0
  53. package/test/ghostty-term.test.js +202 -0
  54. package/test/http500-backoff.test.js +854 -0
  55. package/test/integration.test.js +86 -0
  56. package/test/memory-guard-env.test.js +220 -0
  57. package/test/pr233-fixes.test.js +259 -0
  58. package/test/run-agent-init.js +297 -0
  59. package/test/run-all.js +64 -0
  60. package/test/run-config-flag.js +159 -0
  61. package/test/run-cursor-drift.js +82 -0
  62. package/test/run-session-path.js +154 -0
  63. package/test/run-tests.js +643 -0
  64. package/test/sandbox-redirect.test.js +202 -0
  65. package/test/session-path.test.js +132 -0
  66. package/test/shebang-strip.test.js +241 -0
  67. package/test/soul-reinject.test.js +1027 -0
  68. package/test/soul-reread.test.js +281 -0
  69. package/test/tool-output-formatter.test.js +486 -0
  70. package/test/tool-output-gating.test.js +143 -0
  71. package/test/tool-states.test.js +167 -0
  72. package/test/tools-flag.test.js +65 -0
  73. package/test/tui-attach.test.js +1255 -0
  74. package/test/tui-compaction.test.js +354 -0
  75. package/test/tui-wrap.test.js +568 -0
  76. package/test-binary.js +52 -0
  77. package/test-binary2.js +36 -0
  78. package/LICENSE +0 -21
  79. package/pave.js +0 -2
  80. package/sandbox/SandboxRunner.js +0 -1
  81. package/sandbox/pave-run.js +0 -2
  82. package/sandbox/permission.js +0 -1
  83. package/sandbox/utils/yaml.js +0 -1
@@ -0,0 +1,837 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PAVE CLI Argument Parser
4
+ * Node 16 compatible - no external dependencies
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ /* global PAVE_VERSION */
11
+
12
+ // Shared flag definitions used by multiple command branches (hoisted to avoid duplication)
13
+ const FLAGS_WITH_VALUES = new Set([
14
+ '--config', '--category', '-c', '--model', '-m', '--session', '-s',
15
+ '--port', '-p', '--host', '--max-memory', '--memory-limit', '--memory-warning',
16
+ '--memory-critical', '--memory-startup', '--opencode-dir',
17
+ '--bind', '-b', '--shell', '--last', '--export',
18
+ '--name', '-n', '--sleep', '--soul', '--reinject-interval',
19
+ '--query', '-q', '--skill', '--path', '--output', '-o',
20
+ '--lines', '--agent',
21
+ ]);
22
+ const BOOLEAN_FLAGS = new Set([
23
+ '--json', '--no-stream', '--color', '--verbose', '--no-tools', '--debug',
24
+ '--force', '-f', '--all', '-a', '--help', '-h', '--version', '-v',
25
+ '--list', '--full', '--quiet', '--silent',
26
+ '--daemon', '--follow',
27
+ '--urgent',
28
+ ]);
29
+ // Flags that optionally take a value (on/off/true/false/0/1)
30
+ const OPTIONAL_VALUE_FLAGS = new Set(['--memory-monitor', '--ish-mode']);
31
+ // Include common case variants so --ish-mode TRUE / --memory-monitor ON work
32
+ const OPTIONAL_VALUES = new Set([
33
+ 'on', 'off', 'true', 'false', '0', '1',
34
+ 'ON', 'OFF', 'TRUE', 'FALSE',
35
+ ]);
36
+
37
+ function parseArgs(argv = process.argv.slice(2)) {
38
+ const args = {
39
+ // Subcommand (install, list, remove, etc.) - null means run TUI
40
+ command: null,
41
+ commandArgs: [], // Arguments for the subcommand
42
+
43
+ // Memory configuration
44
+ ishMode: null, // Force ISH mode: true/false/null (auto-detect)
45
+ memoryLimit: null, // Heap limit in MB
46
+ memoryWarning: null, // Warning threshold in MB
47
+ memoryCritical: null, // Critical threshold in MB
48
+ memoryStartup: null, // Startup limit in MB
49
+ memoryMonitor: null, // Enable memory monitoring: true/false/null (auto)
50
+
51
+ // Server configuration
52
+ port: null, // Server port
53
+ host: null, // Server hostname
54
+ opencodedir: null, // Working directory
55
+
56
+ // General options
57
+ help: false,
58
+ version: false,
59
+ debug: false,
60
+ verbose: false,
61
+ force: false, // Force overwrite for install
62
+ json: false, // JSON output for list
63
+
64
+ // Marketplace options
65
+ all: false, // Show all skills (for search)
66
+ category: null, // Filter by category (for search)
67
+ check: false, // Check only, don't install (for update)
68
+ createPr: false, // Create PR for publish
69
+ upgrade: false, // Upgrade existing skill in marketplace
70
+
71
+ // Chat options
72
+ session: null, // Session ID or 'new' for new session
73
+ noStream: false, // Disable streaming output
74
+ color: false, // Enable colored output (implies --no-stream)
75
+ model: null, // Model to use (e.g., 'github-copilot/claude-sonnet-4')
76
+ showTools: true, // Tool output shown by default; use --no-tools to hide
77
+ agent: null, // Target agent name for inbox message (pave chat -a <agent>)
78
+ urgent: false, // Send as urgent interrupt (pave chat -a <agent> --urgent "msg")
79
+
80
+ // Agent options
81
+ name: null, // Agent name (e.g., 'OpenPAVE software engineer')
82
+ sleep: null, // Sleep duration between iterations (e.g., '30m', '1h', '5s')
83
+ soul: null, // Path to SOUL.md file for agent instructions
84
+ reinjectInterval: null, // Re-send SOUL every N iterations to prevent drift; null uses agent default (10)
85
+ agentSubcommand: null, // Agent subcommand: 'init', 'stop', 'logs' for pave agent <sub>
86
+ daemon: false, // Start agent in background (--daemon)
87
+ follow: false, // Follow log output in real-time (--follow)
88
+ lines: null, // Number of log lines to show (--lines <n>)
89
+
90
+ // History options
91
+ last: null, // Show last N messages
92
+ export: null, // Export to file path
93
+ listSessions: false, // List all sessions
94
+
95
+ // Shell-init options
96
+ bind: null, // Key binding (e.g., 'ctrl+g', 'ctrl+p')
97
+ shell: null, // Shell type: zsh, bash, fish (auto-detected)
98
+ installShell: false, // Auto-install to shell config file
99
+
100
+ // Config path
101
+ config: null, // Custom .pave config directory path
102
+ };
103
+
104
+ // Check for subcommands first (must be first non-flag argument)
105
+ const subcommands = ['install', 'list', 'remove', 'uninstall', 'info', 'run', 'search', 'publish', 'update', 'chat', 'history', 'shell-init', 'agent', 'agents'];
106
+ if (argv.length > 0 && subcommands.includes(argv[0])) {
107
+ args.command = argv[0];
108
+
109
+ // For 'run' command, pass ALL remaining arguments (including flags) to the skill
110
+ // For 'chat' command, collect the message (may be multiple words)
111
+ // For other commands, only collect non-flag arguments
112
+ if (args.command === 'run') {
113
+ // Pass everything after 'run' to the skill (it handles its own flags)
114
+ args.commandArgs = argv.slice(1);
115
+ // Return early - don't parse any flags for 'run' command
116
+ // They all belong to the skill
117
+ return args;
118
+ } if (args.command === 'chat') {
119
+ // Collect message parts (everything that's not a flag we handle)
120
+ for (let i = 1; i < argv.length; i++) {
121
+ const a = argv[i];
122
+ // Skip our flags and their values
123
+ if (a === '--session' || a === '-s' || a === '--model' || a === '-m') {
124
+ i++; // Skip the value too
125
+ } else if (a === '--agent' || a === '-a') {
126
+ // Agent target for inbox message (#231: validate value is not a flag)
127
+ if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
128
+ args.agent = argv[i + 1];
129
+ i++; // Skip the value
130
+ } else {
131
+ console.error('Error: --agent requires an agent name');
132
+ process.exit(1);
133
+ }
134
+ } else if (a === '--json' || a === '--no-stream' || a === '--color' || a === '--verbose' || a === '--no-tools' || a === '--debug') {
135
+ // Skip boolean flags
136
+ } else if (!a.startsWith('-')) {
137
+ args.commandArgs.push(a);
138
+ }
139
+ }
140
+ } else if (args.command === 'agent') {
141
+ // For agent command, extract positional args (SOUL path) while skipping flags and their values.
142
+ // Also detect 'init' subcommand: the first non-flag positional token that is 'init'
143
+ // sets agentSubcommand and is excluded from commandArgs.
144
+ // Key insight: the agent command should have exactly one positional arg (the SOUL path).
145
+ // All unknown --flags are assumed to take a value (consumed as next token).
146
+
147
+ for (let i = 1; i < argv.length; i++) {
148
+ const a = argv[i];
149
+ const next = argv[i + 1];
150
+
151
+ // Skip known flags that take values (and their values)
152
+ if (FLAGS_WITH_VALUES.has(a)) {
153
+ i++; // Skip the value too
154
+ } else if (BOOLEAN_FLAGS.has(a)) {
155
+ // Skip boolean flags
156
+ } else if (OPTIONAL_VALUE_FLAGS.has(a)) {
157
+ // Skip optional value flags - check if next arg is an optional value
158
+ if (next && OPTIONAL_VALUES.has(next.toLowerCase())) {
159
+ i++; // Skip the optional value
160
+ }
161
+ } else if (a.startsWith('--') && a.includes('=')) {
162
+ // Handle --flag=value style (skip entirely)
163
+ continue;
164
+ } else if (a.startsWith('-') && !a.startsWith('--') && a.length === 2 && next && !next.startsWith('-')) {
165
+ // Handle unknown short flags like -x value (assume they take a value if next doesn't start with -)
166
+ i++; // Skip the presumed value
167
+ } else if (a.startsWith('--') && next && !next.startsWith('-')) {
168
+ // Handle unknown long flags: assume they take a value whenever next arg exists
169
+ // and doesn't start with '-'. This prevents URIs like discord://123 from being
170
+ // misclassified as positional args.
171
+ i++; // Skip the presumed value
172
+ } else if (!a.startsWith('-')) {
173
+ // Positional argument: either a subcommand keyword (init/stop/start/restart/
174
+ // logs/update/rm/ps) as the first positional, or a SOUL path / agent name after that.
175
+ // Detect agent subcommands if it's the first positional token
176
+ if ((a === 'init' || a === 'stop' || a === 'logs' || a === 'restart' || a === 'start' || a === 'update' || a === 'rm') && args.agentSubcommand === null && args.commandArgs.length === 0) {
177
+ args.agentSubcommand = a;
178
+ } else if (a === 'ps' && args.command === 'agent' && args.agentSubcommand === null && args.commandArgs.length === 0) {
179
+ // Map 'pave agent ps' to 'pave agents' to avoid .pave directory creation side effect
180
+ args.command = 'agents';
181
+ // Skip adding 'ps' to commandArgs; extra args are collected below and validated
182
+ } else if (args.command === 'agents') {
183
+ // Already mapped to agents; collect any remaining args (should be empty)
184
+ args.commandArgs.push(a);
185
+ } else {
186
+ args.commandArgs.push(a);
187
+ }
188
+ }
189
+ // Skip any other flags we don't recognize (flags with no value after them)
190
+ }
191
+
192
+ // Validate positional args for agent commands.
193
+ // For subcommands (stop/start/restart/logs/update/rm), enforce at most one agent name.
194
+ // For bare "pave agent" with multiple positional args, heuristically pick the
195
+ // SOUL file (prefer .md files, fall back to last arg) and warn.
196
+ // 'pave agent ps' maps to 'pave agents' - reject any extra args
197
+ if (args.command === 'agents' && args.commandArgs.length > 0) {
198
+ console.error('Error: "pave agent ps" accepts no arguments, got: ' + args.commandArgs.join(', '));
199
+ console.error('Usage: pave agent ps');
200
+ process.exit(1);
201
+ } else if (args.agentSubcommand === 'stop' || args.agentSubcommand === 'logs' || args.agentSubcommand === 'restart' || args.agentSubcommand === 'start' || args.agentSubcommand === 'update' || args.agentSubcommand === 'rm') {
202
+ // stop/start/restart/logs/update/rm accept at most one agent name
203
+ if (args.commandArgs.length > 1) {
204
+ console.error('Error: "pave agent ' + args.agentSubcommand + '" accepts at most one agent name, got: ' + args.commandArgs.join(', '));
205
+ console.error('Usage: pave agent ' + args.agentSubcommand + ' [name]');
206
+ process.exit(1);
207
+ }
208
+ } else if (!args.agentSubcommand && args.commandArgs.length > 1) {
209
+ const mdFiles = args.commandArgs.filter((a) => a.endsWith('.md'));
210
+ if (mdFiles.length === 1) {
211
+ // Use the .md file as SOUL path, discard others
212
+ args.commandArgs = mdFiles;
213
+ } else if (mdFiles.length > 1) {
214
+ // Multiple .md files - use --soul to disambiguate
215
+ console.error(`Warning: Multiple potential SOUL files found: ${args.commandArgs.join(', ')}`);
216
+ console.error(` Use --soul <path> to specify the SOUL file explicitly.`);
217
+ // Keep the last one as the presumed SOUL path
218
+ args.commandArgs = [args.commandArgs[args.commandArgs.length - 1]];
219
+ } else {
220
+ // No .md files, use the last positional arg
221
+ console.error(`Warning: Multiple positional args found: ${args.commandArgs.join(', ')}`);
222
+ console.error(` Using the last one as SOUL path. Use --soul to be explicit.`);
223
+ args.commandArgs = [args.commandArgs[args.commandArgs.length - 1]];
224
+ }
225
+ }
226
+ } else {
227
+ // Collect remaining non-flag arguments as command args.
228
+ // Skip values that follow flags expecting arguments (e.g., --config ./.pave)
229
+ // Uses shared FLAGS_WITH_VALUES, BOOLEAN_FLAGS, OPTIONAL_VALUE_FLAGS from module scope.
230
+ for (let i = 1; i < argv.length; i++) {
231
+ const a = argv[i];
232
+ const next = argv[i + 1];
233
+ if (FLAGS_WITH_VALUES.has(a)) {
234
+ i++; // skip the flag's value
235
+ } else if (OPTIONAL_VALUE_FLAGS.has(a)) {
236
+ // Skip optional value flags - consume next token only when it matches an allowed value
237
+ if (next && OPTIONAL_VALUES.has(next.toLowerCase())) {
238
+ i++; // Skip the optional value
239
+ }
240
+ } else if (!a.startsWith('-')) {
241
+ args.commandArgs.push(a);
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ for (let i = 0; i < argv.length; i++) {
248
+ const arg = argv[i];
249
+ const next = argv[i + 1];
250
+
251
+ // Context-sensitive short flags for 'agent logs' subcommand.
252
+ // -f means --follow and -n means --lines when inside 'agent logs',
253
+ // overriding their normal meanings (--force, --name).
254
+ if (args.agentSubcommand === 'logs') {
255
+ if (arg === '-f') {
256
+ args.follow = true;
257
+ continue;
258
+ }
259
+ if (arg === '-n') {
260
+ const linesValue = argv[i + 1];
261
+ if (!linesValue || linesValue.startsWith('-')) {
262
+ console.error('Error: -n/--lines requires a positive integer value');
263
+ process.exit(1);
264
+ }
265
+ if (!/^\d+$/.test(linesValue)) {
266
+ console.error('Error: -n/--lines requires a positive integer, got "' + linesValue + '"');
267
+ process.exit(1);
268
+ }
269
+ const parsedLines = parseInt(linesValue, 10);
270
+ if (parsedLines <= 0) {
271
+ console.error('Error: -n/--lines requires a positive integer, got "' + linesValue + '"');
272
+ process.exit(1);
273
+ }
274
+ args.lines = parsedLines;
275
+ i++;
276
+ continue;
277
+ }
278
+ }
279
+
280
+ // Boolean flags
281
+ if (arg === '--help' || arg === '-h') {
282
+ args.help = true;
283
+ } else if (arg === '--version' || arg === '-v') {
284
+ args.version = true;
285
+ } else if (arg === '--debug') {
286
+ args.debug = true;
287
+ } else if (arg === '--verbose') {
288
+ args.verbose = true;
289
+ } else if (arg === '--force' || arg === '-f') {
290
+ args.force = true;
291
+ } else if (arg === '--json') {
292
+ args.json = true;
293
+ } else if (arg === '--all' || (arg === '-a' && args.command !== 'chat' && args.command !== null)) {
294
+ args.all = true;
295
+ } else if (arg === '--check') {
296
+ args.check = true;
297
+ } else if (arg === '--create-pr') {
298
+ args.createPr = true;
299
+ } else if (arg === '--upgrade' || arg === '-u') {
300
+ args.upgrade = true;
301
+ } else if ((arg === '--category' || arg === '-c') && next) {
302
+ args.category = next;
303
+ i++;
304
+ }
305
+
306
+ // Chat options
307
+ else if ((arg === '--session' || arg === '-s') && next) {
308
+ args.session = next;
309
+ i++;
310
+ } else if (arg === '--no-stream') {
311
+ args.noStream = true;
312
+ } else if (arg === '--color') {
313
+ args.color = true;
314
+ args.noStream = true; // Color output requires waiting for full response
315
+ } else if ((arg === '--model' || arg === '-m') && next) {
316
+ args.model = next;
317
+ i++;
318
+ } else if (arg === '--no-tools') {
319
+ args.showTools = false;
320
+ } else if ((arg === '--agent' || arg === '-a') && next && !next.startsWith('-')) {
321
+ args.agent = next;
322
+ i++;
323
+ } else if (arg === '--urgent') {
324
+ args.urgent = true;
325
+ }
326
+
327
+ // Agent options
328
+ else if (arg === '--name' || arg === '-n') {
329
+ const nameValue = argv[i + 1];
330
+ if (!nameValue || nameValue.startsWith('-')) {
331
+ console.error('Error: --name requires a value');
332
+ process.exit(1);
333
+ }
334
+ args.name = nameValue;
335
+ i++;
336
+ } else if ((arg === '--sleep')) {
337
+ const sleepValue = argv[i + 1];
338
+ if (!sleepValue || sleepValue.startsWith('-')) {
339
+ console.error('Error: --sleep requires a value');
340
+ process.exit(1);
341
+ }
342
+ args.sleep = sleepValue;
343
+ i++;
344
+ } else if (arg === '--soul') {
345
+ const soulValue = argv[i + 1];
346
+ if (!soulValue || soulValue.startsWith('-')) {
347
+ console.error('Error: --soul requires a value');
348
+ process.exit(1);
349
+ }
350
+ args.soul = soulValue;
351
+ i++;
352
+ } else if (arg === '--reinject-interval') {
353
+ const riValue = argv[i + 1];
354
+ if (!riValue || riValue.startsWith('-')) {
355
+ console.error('Error: --reinject-interval requires a positive integer value');
356
+ process.exit(1);
357
+ }
358
+ if (!/^\d+$/.test(riValue)) {
359
+ console.error(`Error: --reinject-interval requires a positive integer, got "${riValue}"`);
360
+ process.exit(1);
361
+ }
362
+ const parsed = parseInt(riValue, 10);
363
+ if (parsed <= 0) {
364
+ console.error(`Error: --reinject-interval requires a positive integer, got "${riValue}"`);
365
+ process.exit(1);
366
+ }
367
+ args.reinjectInterval = parsed;
368
+ i++;
369
+ } else if (arg === '--daemon') {
370
+ args.daemon = true;
371
+ } else if (arg === '--follow') {
372
+ args.follow = true;
373
+ } else if (arg === '--lines') {
374
+ const linesValue = argv[i + 1];
375
+ if (!linesValue || linesValue.startsWith('-')) {
376
+ console.error('Error: --lines requires a positive integer value');
377
+ process.exit(1);
378
+ }
379
+ if (!/^\d+$/.test(linesValue)) {
380
+ console.error('Error: --lines requires a positive integer, got "' + linesValue + '"');
381
+ process.exit(1);
382
+ }
383
+ const parsedLines = parseInt(linesValue, 10);
384
+ if (parsedLines <= 0) {
385
+ console.error('Error: --lines requires a positive integer, got "' + linesValue + '"');
386
+ process.exit(1);
387
+ }
388
+ args.lines = parsedLines;
389
+ i++;
390
+ }
391
+
392
+ // History options
393
+ else if (arg === '--list') {
394
+ args.listSessions = true;
395
+ } else if (arg === '--last' && next) {
396
+ const lastValue = parseInt(next, 10);
397
+ if (!Number.isInteger(lastValue) || lastValue <= 0) {
398
+ console.error(`Error: --last requires a positive integer, got "${next}"`);
399
+ process.exit(1);
400
+ }
401
+ args.last = lastValue;
402
+ i++;
403
+ } else if (arg === '--export' && next) {
404
+ args.export = next;
405
+ i++;
406
+ }
407
+
408
+ // Shell-init options
409
+ else if ((arg === '--bind' || arg === '-b') && next) {
410
+ // Validate keybinding: only allow ctrl+<letter>, ^<letter>, or ctrl+/ patterns
411
+ // These are the only formats parseKeyBinding() can reliably convert to shell codes
412
+ const validBindPattern = /^(ctrl[+\-_][a-zA-Z\/\\]|\^[a-zA-Z])$/i;
413
+ if (!validBindPattern.test(next)) {
414
+ console.error(`Error: --bind must be in format: ctrl+<letter> or ^<letter> (e.g., ctrl+p, ^G)`);
415
+ process.exit(1);
416
+ }
417
+ args.bind = next;
418
+ i++;
419
+ } else if (arg === '--shell' && next) {
420
+ // Validate shell type
421
+ const validShells = ['zsh', 'bash', 'fish'];
422
+ if (!validShells.includes(next.toLowerCase())) {
423
+ console.error(`Error: --shell must be one of: ${validShells.join(', ')}`);
424
+ process.exit(1);
425
+ }
426
+ args.shell = next.toLowerCase();
427
+ i++;
428
+ } else if (arg === '--install' || arg === '-i') {
429
+ args.installShell = true;
430
+ }
431
+
432
+ // ISH Memory Configuration
433
+ else if (arg === '--ish-mode') {
434
+ if (next === 'true' || next === 'on' || next === '1') {
435
+ args.ishMode = true;
436
+ } else if (next === 'false' || next === 'off' || next === '0') {
437
+ args.ishMode = false;
438
+ } else {
439
+ args.ishMode = true; // Default to true if no value
440
+ }
441
+ if (next && (next === 'true' || next === 'false' || next === 'on' || next === 'off' || next === '1' || next === '0')) {
442
+ i++; // Skip next arg if it was consumed
443
+ }
444
+ }
445
+
446
+ else if (arg === '--memory-limit' && next) {
447
+ const mb = parseInt(next, 10);
448
+ if (!isNaN(mb) && mb > 0) {
449
+ args.memoryLimit = mb;
450
+ i++;
451
+ }
452
+ }
453
+
454
+ else if (arg === '--memory-warning' && next) {
455
+ const mb = parseInt(next, 10);
456
+ if (!isNaN(mb) && mb > 0) {
457
+ args.memoryWarning = mb;
458
+ i++;
459
+ }
460
+ }
461
+
462
+ else if (arg === '--memory-critical' && next) {
463
+ const mb = parseInt(next, 10);
464
+ if (!isNaN(mb) && mb > 0) {
465
+ args.memoryCritical = mb;
466
+ i++;
467
+ }
468
+ }
469
+
470
+ else if (arg === '--memory-startup' && next) {
471
+ const mb = parseInt(next, 10);
472
+ if (!isNaN(mb) && mb > 0) {
473
+ args.memoryStartup = mb;
474
+ i++;
475
+ }
476
+ }
477
+
478
+ else if (arg === '--memory-monitor') {
479
+ if (next === 'true' || next === 'on' || next === '1') {
480
+ args.memoryMonitor = true;
481
+ } else if (next === 'false' || next === 'off' || next === '0') {
482
+ args.memoryMonitor = false;
483
+ } else {
484
+ args.memoryMonitor = true; // Default to true if no value
485
+ }
486
+ if (next && (next === 'true' || next === 'false' || next === 'on' || next === 'off' || next === '1' || next === '0')) {
487
+ i++;
488
+ }
489
+ }
490
+
491
+ // Server Configuration
492
+ else if (arg === '--port' && next) {
493
+ const port = parseInt(next, 10);
494
+ if (!isNaN(port) && port > 0 && port < 65536) {
495
+ args.port = port;
496
+ i++;
497
+ }
498
+ }
499
+
500
+ else if (arg === '--host' && next) {
501
+ args.host = next;
502
+ i++;
503
+ }
504
+
505
+ else if (arg === '--opencode-dir' && next) {
506
+ args.opencodedir = next;
507
+ i++;
508
+ }
509
+
510
+ // Config path - ensure next is a value, not another flag
511
+ else if (arg === '--config') {
512
+ if (next && !next.startsWith('-')) {
513
+ args.config = next;
514
+ i++;
515
+ } else {
516
+ console.error('Error: --config requires a path argument');
517
+ process.exit(1);
518
+ }
519
+ }
520
+
521
+ // Handle combined short flags
522
+ else if (arg.startsWith('-') && !arg.startsWith('--') && arg.length > 2) {
523
+ for (let j = 1; j < arg.length; j++) {
524
+ const flag = arg[j];
525
+ if (flag === 'h') args.help = true;
526
+ else if (flag === 'v') args.version = true;
527
+ else if (flag === 'd') args.debug = true;
528
+ }
529
+ }
530
+ }
531
+
532
+ // Apply defaults for agent command for any omitted options
533
+ // Each option is defaulted independently if not explicitly provided
534
+ // Skip defaults when --help or --version is requested to avoid side effects (e.g., directory creation)
535
+ if (args.command === 'agent' && !args.help && !args.version) {
536
+ // Default config: ./.pave (relative to current working directory)
537
+ if (!args.config) {
538
+ args.config = './.pave';
539
+ }
540
+
541
+ // For 'agent init/stop/logs', only config default is needed (no sleep/SOUL defaults)
542
+ if (!args.agentSubcommand) {
543
+ // Default sleep: 1m (1 minute)
544
+ if (!args.sleep) {
545
+ args.sleep = '1m';
546
+ }
547
+
548
+ // Default SOUL file: AGENTS.md (if no soul flag and no command args)
549
+ if (!args.soul && args.commandArgs.length === 0) {
550
+ args.commandArgs = ['AGENTS.md'];
551
+ }
552
+ }
553
+ }
554
+
555
+ return args;
556
+ }
557
+
558
+ function showHelp() {
559
+ console.log(`
560
+ PAVE - Personal AI Virtual Environment
561
+
562
+ USAGE:
563
+ pave [options] Start the PAVE TUI + server
564
+ pave chat <message> Send a message (non-interactive)
565
+ pave agent [options] [SOUL.md] Run as continuous autonomous agent (default: AGENTS.md)
566
+ pave agent --daemon Start agent in background (daemon mode)
567
+ pave agent init Initialize agent environment (.pave + permissions.yaml)
568
+ pave agent stop [name] Stop a running agent (SIGTERM, SIGKILL after 5s)
569
+ pave agent start [name] Start a stopped agent using saved config from status.json
570
+ pave agent restart [name] Restart a running agent (preserves session)
571
+ pave agent update [name] [--sleep <dur>] [--reinject-interval <n>] [--soul <path>]
572
+ Update agent config (sleep, reinject interval, soul)
573
+ pave agent logs [name] [-f|--follow] [-n|--lines <n>] View agent log output
574
+ pave agent ps List all registered agent instances (alias: pave agents)
575
+ pave agents List all registered agent instances (alias: pave agent ps)
576
+ pave history [options] Show session conversation history
577
+ pave shell-init [options] Output shell integration code
578
+ pave run <skill> <command> [args] Run a skill command
579
+ pave install <name|path|url> Install a skill
580
+ pave search <query> Search the skill marketplace
581
+ pave publish [path] Publish a skill to marketplace
582
+ pave update <name> Update an installed skill
583
+ pave list [options] List installed skills
584
+ pave remove <name> Remove an installed skill
585
+ pave info <name> Show info about a skill
586
+
587
+ SHELL INTEGRATION:
588
+ shell-init Output shell integration code
589
+
590
+ --bind, -b <key> Key binding (default: ctrl+p)
591
+ --shell <shell> Shell type: zsh, bash, fish (auto-detected)
592
+ --install, -i Auto-install to shell config (~/.zshrc, etc.)
593
+
594
+ Setup:
595
+ # Auto-install (recommended)
596
+ pave shell-init --install
597
+
598
+ # Or manually add to your shell config:
599
+ # Zsh (~/.zshrc)
600
+ eval "$(pave shell-init)"
601
+
602
+ # Bash (~/.bashrc)
603
+ eval "$(pave shell-init --shell bash)"
604
+
605
+ # Fish (~/.config/fish/config.fish)
606
+ pave shell-init --shell fish | source
607
+
608
+ Then press Ctrl+P to start chatting inline!
609
+
610
+ CHAT (Non-Interactive):
611
+ chat <message> Send a message and get response
612
+ chat "multi word msg" Use quotes for multi-word messages
613
+ echo "msg" | pave chat Pipe input as message
614
+
615
+ --session, -s <id> Use specific session ID
616
+ --session new Force new session (ignore .pave-session.json)
617
+ --model, -m <model> Model to use (e.g., github-copilot/claude-sonnet-4)
618
+ --agent, -a <name> Send message to a running agent's inbox
619
+ --urgent Send as urgent interrupt (aborts current work)
620
+ --no-tools Hide tool execution progress/results (shown by default)
621
+ --no-stream Wait for complete response (no streaming)
622
+ --color Colored markdown output (implies --no-stream)
623
+ --json Output response as JSON
624
+
625
+ AGENT MODE (Continuous Autonomous Agent):
626
+ agent [SOUL.md] Run agent following instructions (default: AGENTS.md)
627
+ agent init Initialize agent environment (create .pave + default permissions.yaml)
628
+ agent init --config <dir> Initialize at a custom config directory
629
+ agent --soul <path> Alternative way to specify SOUL file
630
+
631
+ --name, -n <name> Agent name (e.g., 'OpenPAVE software engineer')
632
+ --sleep <duration> Sleep between iterations (e.g., '30m', '1h', '5s')
633
+ Default: 1m (1 minute)
634
+ --reinject-interval <n> Re-send SOUL every N iterations to prevent drift
635
+ Default: 10
636
+ --config <path> Config directory (default: ./.pave)
637
+ --session, -s <id> Use existing session (resumes conversation)
638
+ --session new Force new session (re-sends SOUL instructions)
639
+ --model, -m <model> Model to use
640
+
641
+ Defaults: SOUL=AGENTS.md, sleep=1m, reinject-interval=10, config=./.pave
642
+
643
+ The agent runs in a continuous loop:
644
+ 1. On startup, reads SOUL.md for instructions (personality, goals, channels)
645
+ 2. If starting a NEW session: sends SOUL.md content as first message
646
+ If RESUMING a session: skips initial SOUL send, starts with "keep going"
647
+ 3. SOUL is re-injected automatically when:
648
+ - The SOUL file is edited (hot-reload via content hash detection)
649
+ - After auto-compaction (to restore full instructions in new session)
650
+ - Every N iterations (periodic, controlled by --reinject-interval)
651
+ 4. Between SOUL re-injections, sends "keep going" to continue
652
+ 5. Sleeps for the specified duration between iterations
653
+ 6. Repeats until stopped (Ctrl+C)
654
+
655
+ Session Persistence:
656
+ - Agent sessions are stored in ~/.pave/state/agent-session-<hash>.json
657
+ - Restarting the agent will resume the previous session automatically
658
+ - Use --session new to force a fresh session (re-sends SOUL instructions)
659
+
660
+ Example SOUL.md:
661
+ You are an AI software engineer for the OpenPAVE project.
662
+
663
+ Your goals:
664
+ 1. Check GitHub issues: gh issue list --repo cnrai/openpave
665
+ 2. Check open PRs: gh pr list --repo cnrai/openpave
666
+ 3. If there are issues assigned to you or unassigned, work on them
667
+ 4. Create PRs for fixes, monitor Copilot reviews
668
+
669
+ HISTORY:
670
+ history Show conversation history for current session
671
+ history --session <id> Show history for specific session
672
+ history --list List all available sessions
673
+ history --last <n> Show only last N messages (default: all)
674
+ history --export <file> Export history to file (markdown format)
675
+ history --json Output as JSON
676
+
677
+ SKILL EXECUTION:
678
+ run <skill> <command> [args...] Run a skill command in the sandbox
679
+ Example: pave run gmail list --max 5
680
+
681
+ MARKETPLACE:
682
+ search <query> Search marketplace by name/keywords
683
+ search --all List all available skills
684
+ search -c <category> Filter by category
685
+ install <name> Install from marketplace (e.g., pave install gmail)
686
+ install <owner/repo> Install from GitHub (e.g., pave install cnrai/openpave-gmail)
687
+ install <path> Install from local directory
688
+ publish [path] Validate and publish skill to marketplace
689
+ publish --create-pr Auto-create PR to marketplace (requires gh CLI)
690
+ publish --upgrade Upgrade existing skill in marketplace
691
+ update <name> Update skill to latest version
692
+ update --all Update all installed skills
693
+ update --check Check for updates without installing
694
+
695
+ SKILL MANAGEMENT:
696
+ list List all installed skills
697
+ remove <name> Remove/uninstall a skill
698
+ info <name> Show detailed skill information
699
+
700
+ SKILL OPTIONS:
701
+ --force, -f Force overwrite existing skill
702
+ --json Output in JSON format
703
+ --verbose Show detailed execution info
704
+ --all, -a Show all (for search/update)
705
+ --category, -c <cat> Filter by category
706
+ --check Check only, don't install
707
+ --create-pr Create PR for publishing
708
+ --upgrade, -u Upgrade existing skill in marketplace
709
+
710
+ MEMORY CONFIGURATION (ISH/Resource-Constrained Environments):
711
+ --ish-mode [on|off] Force ISH mode (auto-detected by default)
712
+ --memory-limit <MB> Heap memory limit in MB (default: 8MB for ISH, 100MB standard)
713
+ --memory-warning <MB> Warning threshold in MB (default: 80% of limit)
714
+ --memory-critical <MB> Critical threshold in MB (default: same as limit)
715
+ --memory-startup <MB> Startup limit in MB (default: higher to allow init)
716
+ --memory-monitor [on|off] Enable memory monitoring (auto-enabled for ISH)
717
+
718
+ SERVER CONFIGURATION:
719
+ --port <port> Server port (default: 4096)
720
+ --host <hostname> Server hostname (default: 127.0.0.1)
721
+ --opencode-dir <path> Working directory (default: current)
722
+
723
+ GENERAL OPTIONS:
724
+ --config <path> Custom .pave config directory
725
+ Default: ~/.pave (global)
726
+ For 'agent' command: ./.pave (project-local, allows per-project config)
727
+ --debug Enable debug logging
728
+ --verbose Verbose output
729
+ --help, -h Show this help
730
+ --version, -v Show version
731
+
732
+ EXAMPLES:
733
+ # Start PAVE TUI
734
+ pave
735
+
736
+ # Non-interactive chat
737
+ pave chat "What is 2+2?"
738
+ pave chat "Explain this error" --session new
739
+ pave chat "Hello" --model github-copilot/claude-sonnet-4
740
+ echo "Summarize this" | pave chat
741
+ cat file.txt | pave chat "Review this code"
742
+
743
+ # Send message to a running agent
744
+ pave chat -a github-watcher "check the error logs"
745
+ pave chat --agent gw "check logs" # prefix matching
746
+ pave chat -a gw --urgent "stop, check production errors"
747
+
748
+ # Search marketplace
749
+ pave search email
750
+ pave search --all
751
+ pave search -c communication
752
+
753
+ # Install from marketplace
754
+ pave install gmail
755
+ pave install dropbox
756
+
757
+ # Install from GitHub
758
+ pave install cnrai/openpave-gmail
759
+
760
+ # Run a skill command
761
+ pave run gmail list --max 5
762
+ pave run gmail unread
763
+ pave run gmail read <messageId>
764
+
765
+ # Publish a skill
766
+ pave publish ./my-skill
767
+ pave publish --create-pr
768
+ pave publish --create-pr --upgrade # Upgrade existing skill
769
+
770
+ # Update skills
771
+ pave update gmail
772
+ pave update --all
773
+
774
+ # List installed skills
775
+ pave list
776
+ pave list --json
777
+
778
+ # Remove a skill
779
+ pave remove gmail
780
+
781
+ # Use custom config directory (for agent command)
782
+ # Note: The agent command supports --config as an option
783
+ pave agent --config /tmp/.pave SOUL.md
784
+ pave agent --config "$HOME/work/.pave" --sleep 30m
785
+
786
+ # View conversation history
787
+ pave history # Current session history
788
+ pave history --list # List all sessions
789
+ pave history --session <id> # Specific session
790
+ pave history --last 10 # Last 10 messages
791
+ pave history --export chat.md # Export to markdown file
792
+ pave history --json # Output as JSON
793
+
794
+ # Run as autonomous agent
795
+ pave agent # Run with defaults (AGENTS.md, 1m sleep, ./.pave)
796
+ pave agent init # Initialize .pave directory with default permissions
797
+ pave agent init --config ./my-config # Initialize at custom config path
798
+ pave agent SOUL.md --sleep 30m # Check every 30 minutes
799
+ pave agent SOUL.md --name "OpenPAVE software engineer"
800
+ pave agent --soul ./agents/github-watcher.md --sleep 1h
801
+
802
+ SKILL LOCATIONS:
803
+ Skills installed to: ~/.pave/skills/<name>/
804
+ Lock file: ~/.pave/skills.lock.json
805
+ Registry cache: ~/.pave/cache/registry.yaml
806
+ (Use --config to override ~/.pave)
807
+ `);
808
+ }
809
+
810
+ function showVersion() {
811
+ // PAVE_VERSION is injected at build time by esbuild define
812
+ // It gets replaced with the actual version string
813
+ let version = '0.0.0'; // fallback
814
+
815
+ // Check if PAVE_VERSION was injected (it becomes a string literal)
816
+ if (typeof PAVE_VERSION === 'string') {
817
+ version = PAVE_VERSION;
818
+ } else {
819
+ // Fallback: try to read package.json (for development)
820
+ try {
821
+ const pkgPath = path.join(__dirname, '..', 'package.json');
822
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
823
+ version = pkg.version || '0.0.0';
824
+ } catch (e) {
825
+ // Use fallback
826
+ }
827
+ }
828
+
829
+ console.log(`PAVE version ${version}`);
830
+ process.exit(0);
831
+ }
832
+
833
+ module.exports = {
834
+ parseArgs,
835
+ showHelp,
836
+ showVersion,
837
+ };