@crewx/cli 0.8.1-rc.4 → 0.8.2-rc.1

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/crewx CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
2
- require('../dist/main.js');
1
+ #!/usr/bin/env node
2
+ require('../dist/main.js');
@@ -1,12 +1,18 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.createCliCrewx = createCliCrewx;
4
7
  const fs_1 = require("fs");
5
8
  const path_1 = require("path");
9
+ const os_1 = require("os");
10
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
6
11
  const sdk_1 = require("@crewx/sdk");
7
12
  const plugins_1 = require("@crewx/sdk/plugins");
8
13
  const register_builtin_tools_1 = require("../register-builtin-tools");
9
14
  const version_1 = require("../utils/version");
15
+ const tasks_1 = require("../schema/tasks");
10
16
  /**
11
17
  * Build a Crewx instance with CLI-standard plugins (FileLogger + SqliteTracing)
12
18
  * and built-in tools registered. Use this from any CLI command that needs a
@@ -39,6 +45,12 @@ async function createCliCrewx(configPath = process.env.CREWX_CONFIG ?? 'crewx.ya
39
45
  // No explicit config and file doesn't exist — built-in-only mode
40
46
  yamlPath = undefined;
41
47
  }
48
+ // Ensure ~/.crewx/crewx.db schema exists before SqliteTracingPlugin opens it.
49
+ const dbDir = (0, path_1.join)((0, os_1.homedir)(), '.crewx');
50
+ (0, fs_1.mkdirSync)(dbDir, { recursive: true });
51
+ const schemaDb = new better_sqlite3_1.default((0, path_1.join)(dbDir, 'crewx.db'));
52
+ (0, tasks_1.ensureTasksSchema)(schemaDb);
53
+ schemaDb.close();
42
54
  // Pass ourselves back to the SDK: when this Crewx encounters a file:// remote
43
55
  // agent, the SDK uses this factory to bootstrap the target Crewx instance
44
56
  // with the same plugin set (FileLogger + SqliteTracing + built-in tools).
package/dist/builtin.js CHANGED
@@ -51,7 +51,7 @@ const BUILTIN_MAP = {
51
51
  wbs: () => Promise.resolve().then(() => __importStar(require('@crewx/wbs/cli'))),
52
52
  cron: () => Promise.resolve().then(() => __importStar(require('@crewx/cron/cli'))),
53
53
  workflow: () => Promise.resolve().then(() => __importStar(require('@crewx/workflow/cli'))),
54
- // NOTE: @crewx/skill does not expose /cli module — handled separately in main.ts
54
+ skill: () => Promise.resolve().then(() => __importStar(require('@crewx/skill/cli'))),
55
55
  };
56
56
  exports.BUILTIN_COMMANDS = new Set(Object.keys(BUILTIN_MAP));
57
57
  // Load skill-tracer for observability (graceful degradation if unavailable)
@@ -219,28 +219,28 @@ function loadAgentSkills() {
219
219
  }
220
220
  }
221
221
  function printAgentHelp() {
222
- console.log(`
223
- CrewX Agent Management
224
-
225
- Usage:
226
- crewx agent # List configured agents (default)
227
- crewx agent ls # List configured agents
228
- crewx agent list # Alias for ls
229
- crewx agent prompt <id> # Inspect rendered agent prompt
230
-
231
- Filter Options (for agent ls):
232
- --role <value> Filter by agent role (comma-separated for multiple: PM,Dev)
233
- --team <value> Filter by agent team (comma-separated for multiple)
234
- --provider <value> Filter by provider (comma-separated, partial match: claude)
235
-
236
- Examples:
237
- crewx agent
238
- crewx agent ls
239
- crewx agent ls --role=PM
240
- crewx agent ls --team="CrewX Core 개발팀"
241
- crewx agent ls --provider=claude
242
- crewx agent ls --role=PM --provider=claude
243
- crewx agent ls --role=PM,general
244
- CREWX_CONFIG=./crewx.yaml crewx agent list
222
+ console.log(`
223
+ CrewX Agent Management
224
+
225
+ Usage:
226
+ crewx agent # List configured agents (default)
227
+ crewx agent ls # List configured agents
228
+ crewx agent list # Alias for ls
229
+ crewx agent prompt <id> # Inspect rendered agent prompt
230
+
231
+ Filter Options (for agent ls):
232
+ --role <value> Filter by agent role (comma-separated for multiple: PM,Dev)
233
+ --team <value> Filter by agent team (comma-separated for multiple)
234
+ --provider <value> Filter by provider (comma-separated, partial match: claude)
235
+
236
+ Examples:
237
+ crewx agent
238
+ crewx agent ls
239
+ crewx agent ls --role=PM
240
+ crewx agent ls --team="CrewX Core 개발팀"
241
+ crewx agent ls --provider=claude
242
+ crewx agent ls --role=PM --provider=claude
243
+ crewx agent ls --role=PM,general
244
+ CREWX_CONFIG=./crewx.yaml crewx agent list
245
245
  `.trim());
246
246
  }
@@ -56,27 +56,27 @@ function escapeYamlBlock(text) {
56
56
  }
57
57
  function generateDefaultYaml(agents) {
58
58
  const agentBlocks = agents
59
- .map((a) => ` - id: "${a.id}"
60
- name: "${a.name}"
61
- role: "${a.role}"
62
- team: "${a.team}"
63
- provider: "${a.provider}"
64
- default_model: "${a.default_model}"
65
- working_directory: "${a.working_directory}"
66
- description: "${a.description}"
67
- system_prompt: |
59
+ .map((a) => ` - id: "${a.id}"
60
+ name: "${a.name}"
61
+ role: "${a.role}"
62
+ team: "${a.team}"
63
+ provider: "${a.provider}"
64
+ default_model: "${a.default_model}"
65
+ working_directory: "${a.working_directory}"
66
+ description: "${a.description}"
67
+ system_prompt: |
68
68
  ${escapeYamlBlock(a.system_prompt)}`)
69
69
  .join('\n\n');
70
- return `# CrewX Agents Configuration
71
- # Generated by 'crewx init'
72
-
73
- agents:
74
- ${agentBlocks}
75
-
76
- # Usage examples:
77
- # crewx query "@planner analyze this codebase"
78
- # crewx execute "@developer implement the feature described above"
79
- # crewx agent ls
70
+ return `# CrewX Agents Configuration
71
+ # Generated by 'crewx init'
72
+
73
+ agents:
74
+ ${agentBlocks}
75
+
76
+ # Usage examples:
77
+ # crewx query "@planner analyze this codebase"
78
+ # crewx execute "@developer implement the feature described above"
79
+ # crewx agent ls
80
80
  `;
81
81
  }
82
82
  /**
@@ -3,7 +3,11 @@
3
3
  *
4
4
  * Supports both `--flag=value` and `--flag value` forms.
5
5
  * Handles: --thread, --provider, --metadata, --verbose, --config/-c,
6
- * --output-format, --effort.
6
+ * --output-format, --effort, --prompt-file/-f.
7
+ *
8
+ * Strict mode: unknown --xxx tokens after known flags are consumed throw an
9
+ * error instead of silently leaking into the message prompt.
10
+ * Use `--` sentinel to pass literal flag-like tokens as positional args.
7
11
  */
8
12
  export interface CommonFlags {
9
13
  /** Thread name for conversation continuity. */
@@ -22,12 +26,24 @@ export interface CommonFlags {
22
26
  effort?: string;
23
27
  /** Path to a file whose content is used as the prompt body (-f/--prompt-file). */
24
28
  promptFile?: string;
25
- /** Remaining non-flag arguments. */
29
+ /** Remaining non-flag positional arguments. */
26
30
  rest: string[];
27
31
  }
32
+ /**
33
+ * Thrown by parseCommonFlags when an unrecognized --xxx flag is encountered.
34
+ * Callers can distinguish this from other errors to map to exit code 2.
35
+ */
36
+ export declare class UnknownOptionError extends Error {
37
+ constructor(message: string);
38
+ }
28
39
  /**
29
40
  * Parse common CLI flags from an args array.
30
- * Returns parsed flags and remaining positional arguments.
41
+ *
42
+ * Strict mode: any unconsumed `--xxx` token (outside a `--` escape block)
43
+ * throws `UnknownOptionError: Unknown option: --xxx.`
44
+ *
45
+ * Tokens after `--` are treated as positional and included in `rest` as-is.
46
+ * The `--` sentinel itself is NOT included in `rest`.
31
47
  */
32
48
  export declare function parseCommonFlags(args: string[]): CommonFlags;
33
49
  /**
@@ -4,9 +4,14 @@
4
4
  *
5
5
  * Supports both `--flag=value` and `--flag value` forms.
6
6
  * Handles: --thread, --provider, --metadata, --verbose, --config/-c,
7
- * --output-format, --effort.
7
+ * --output-format, --effort, --prompt-file/-f.
8
+ *
9
+ * Strict mode: unknown --xxx tokens after known flags are consumed throw an
10
+ * error instead of silently leaking into the message prompt.
11
+ * Use `--` sentinel to pass literal flag-like tokens as positional args.
8
12
  */
9
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.UnknownOptionError = void 0;
10
15
  exports.parseCommonFlags = parseCommonFlags;
11
16
  exports.parseMetadata = parseMetadata;
12
17
  /**
@@ -35,9 +40,25 @@ function parseFlag(args, ...names) {
35
40
  function hasFlag(args, ...names) {
36
41
  return names.some(name => args.includes(name));
37
42
  }
43
+ /**
44
+ * Thrown by parseCommonFlags when an unrecognized --xxx flag is encountered.
45
+ * Callers can distinguish this from other errors to map to exit code 2.
46
+ */
47
+ class UnknownOptionError extends Error {
48
+ constructor(message) {
49
+ super(message);
50
+ this.name = 'UnknownOptionError';
51
+ }
52
+ }
53
+ exports.UnknownOptionError = UnknownOptionError;
38
54
  /**
39
55
  * Parse common CLI flags from an args array.
40
- * Returns parsed flags and remaining positional arguments.
56
+ *
57
+ * Strict mode: any unconsumed `--xxx` token (outside a `--` escape block)
58
+ * throws `UnknownOptionError: Unknown option: --xxx.`
59
+ *
60
+ * Tokens after `--` are treated as positional and included in `rest` as-is.
61
+ * The `--` sentinel itself is NOT included in `rest`.
41
62
  */
42
63
  function parseCommonFlags(args) {
43
64
  const thread = parseFlag(args, '--thread');
@@ -48,7 +69,7 @@ function parseCommonFlags(args) {
48
69
  const effort = parseFlag(args, '--effort');
49
70
  const promptFile = parseFlag(args, '--prompt-file', '-f');
50
71
  const verbose = hasFlag(args, '--verbose');
51
- // Collect the set of consumed positions
72
+ // Collect consumed positions for known flags
52
73
  const consumed = new Set();
53
74
  const flagPairs = [
54
75
  { names: ['--thread'] },
@@ -61,12 +82,10 @@ function parseCommonFlags(args) {
61
82
  ];
62
83
  for (let i = 0; i < args.length; i++) {
63
84
  const arg = args[i];
64
- // Boolean flags
65
85
  if (arg === '--verbose') {
66
86
  consumed.add(i);
67
87
  continue;
68
88
  }
69
- // Value flags
70
89
  for (const { names } of flagPairs) {
71
90
  for (const name of names) {
72
91
  if (arg.startsWith(`${name}=`)) {
@@ -80,7 +99,28 @@ function parseCommonFlags(args) {
80
99
  }
81
100
  }
82
101
  }
83
- const rest = args.filter((_, i) => !consumed.has(i));
102
+ // Build rest with strict unknown-flag detection and -- escape support
103
+ const rest = [];
104
+ let escapeMode = false;
105
+ for (let i = 0; i < args.length; i++) {
106
+ if (consumed.has(i))
107
+ continue;
108
+ const token = args[i];
109
+ if (!escapeMode && token === '--') {
110
+ escapeMode = true; // consume -- sentinel; everything after is positional
111
+ continue;
112
+ }
113
+ if (!escapeMode && token.startsWith('--')) {
114
+ throw new UnknownOptionError(`Unknown option: ${token}. Use "--" to pass literal flag-like tokens as message.\n` +
115
+ ` Did you mean: crewx serve ${token}\n` +
116
+ ` or: crewx q -- ${token} <message> (to include in message)`);
117
+ }
118
+ // Reject short -x flags that are not numeric (e.g. -f was consumed above)
119
+ if (!escapeMode && token.startsWith('-') && token.length > 1 && !/^-?\d/.test(token)) {
120
+ throw new UnknownOptionError(`Unknown option: ${token}`);
121
+ }
122
+ rest.push(token);
123
+ }
84
124
  return { thread, provider, metadata, verbose, config, outputFormat, effort, promptFile, rest };
85
125
  }
86
126
  /**
@@ -0,0 +1,13 @@
1
+ /**
2
+ * CLI command registry — SSOT for commands handled by the CLI engine.
3
+ *
4
+ * Consumed by:
5
+ * - main.ts dispatch (to avoid switch/case literal drift)
6
+ * - bin/__tests__/crewx-entrypoint.test.ts (SSOT assertion vs CLI_SUBCOMMANDS)
7
+ */
8
+ /** Core commands handled by the main.ts switch/case or hook routing. */
9
+ export declare const KNOWN_COMMANDS: Set<string>;
10
+ /** Built-in tool commands routed via handleBuiltin(). */
11
+ export declare const BUILTIN_COMMAND_NAMES: Set<string>;
12
+ /** Commands not yet migrated from cli-bak — show a migration message. */
13
+ export declare const NOT_YET_MIGRATED: Set<string>;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ /**
3
+ * CLI command registry — SSOT for commands handled by the CLI engine.
4
+ *
5
+ * Consumed by:
6
+ * - main.ts dispatch (to avoid switch/case literal drift)
7
+ * - bin/__tests__/crewx-entrypoint.test.ts (SSOT assertion vs CLI_SUBCOMMANDS)
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.NOT_YET_MIGRATED = exports.BUILTIN_COMMAND_NAMES = exports.KNOWN_COMMANDS = void 0;
11
+ /** Core commands handled by the main.ts switch/case or hook routing. */
12
+ exports.KNOWN_COMMANDS = new Set([
13
+ 'q', 'query',
14
+ 'x', 'execute',
15
+ 'agent',
16
+ 'ps', 'kill', 'result', 'log',
17
+ 'doctor', 'init',
18
+ 'help',
19
+ 'slack', 'slack:files',
20
+ 'hook', 'hook-dispatch',
21
+ ]);
22
+ /** Built-in tool commands routed via handleBuiltin(). */
23
+ exports.BUILTIN_COMMAND_NAMES = new Set([
24
+ 'memory', 'search', 'doc', 'wbs', 'cron', 'workflow', 'skill',
25
+ ]);
26
+ /** Commands not yet migrated from cli-bak — show a migration message. */
27
+ exports.NOT_YET_MIGRATED = new Set([
28
+ 'template', 'templates', 'chat', 'mcp',
29
+ ]);
@@ -33,8 +33,8 @@ function getRunningTasks(dbRoot) {
33
33
  if (!db)
34
34
  return [];
35
35
  try {
36
- return db.prepare(`SELECT id, agent_id, prompt, mode, status, pid, started_at, completed_at,
37
- result, error, duration_ms
36
+ return db.prepare(`SELECT id, agent_id, prompt, mode, status, pid, started_at, completed_at,
37
+ result, error, duration_ms
38
38
  FROM tasks WHERE status = 'running' ORDER BY started_at DESC`).all();
39
39
  }
40
40
  finally {
@@ -47,8 +47,8 @@ function getAllTasks(dbRoot) {
47
47
  if (!db)
48
48
  return [];
49
49
  try {
50
- return db.prepare(`SELECT id, agent_id, prompt, mode, status, pid, started_at, completed_at,
51
- result, error, duration_ms
50
+ return db.prepare(`SELECT id, agent_id, prompt, mode, status, pid, started_at, completed_at,
51
+ result, error, duration_ms
52
52
  FROM tasks ORDER BY started_at DESC`).all();
53
53
  }
54
54
  finally {
@@ -61,8 +61,8 @@ function getTask(id, dbRoot) {
61
61
  if (!db)
62
62
  return undefined;
63
63
  try {
64
- return db.prepare(`SELECT id, agent_id, prompt, mode, status, pid, started_at, completed_at,
65
- result, error, duration_ms
64
+ return db.prepare(`SELECT id, agent_id, prompt, mode, status, pid, started_at, completed_at,
65
+ result, error, duration_ms
66
66
  FROM tasks WHERE id = ?`).get(id);
67
67
  }
68
68
  finally {
@@ -97,7 +97,7 @@ function killTask(id, dbRoot) {
97
97
  // ESRCH = process already gone — still clean up the record
98
98
  }
99
99
  }
100
- db.prepare(`UPDATE tasks SET status='failed', error='killed by user',
100
+ db.prepare(`UPDATE tasks SET status='failed', error='killed by user',
101
101
  completed_at=?, pid=NULL WHERE id=?`).run(new Date().toISOString(), id);
102
102
  return { ok: true, message: `Killed task ${id}${task.pid ? ` (PID: ${task.pid})` : ''}` };
103
103
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * DenyIfTouchesSecretsPlugin — Phase 0 demo HookPlugin.
3
+ *
4
+ * Denies Bash tool calls whose command string contains ".env".
5
+ * Pure string matching — trivially bypassable (see README).
6
+ *
7
+ * SECURITY NOTE:
8
+ * Do NOT include tool.input content in deny reasons.
9
+ * Use static messages only to prevent prompt injection.
10
+ * ✅ return ctx.deny('Secrets-related command');
11
+ * ❌ return ctx.deny(`Blocked: ${cmd}`);
12
+ */
13
+ import { HookPlugin } from '@crewx/sdk/hooks';
14
+ import type { HookContext, HookResult } from '@crewx/sdk/hooks';
15
+ export declare class DenyIfTouchesSecretsPlugin extends HookPlugin {
16
+ readonly name = "deny-secrets";
17
+ readonly version = "0.1.0";
18
+ readonly capabilities: {
19
+ required: readonly ["deny"];
20
+ };
21
+ run(ctx: HookContext): Promise<HookResult>;
22
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * DenyIfTouchesSecretsPlugin — Phase 0 demo HookPlugin.
4
+ *
5
+ * Denies Bash tool calls whose command string contains ".env".
6
+ * Pure string matching — trivially bypassable (see README).
7
+ *
8
+ * SECURITY NOTE:
9
+ * Do NOT include tool.input content in deny reasons.
10
+ * Use static messages only to prevent prompt injection.
11
+ * ✅ return ctx.deny('Secrets-related command');
12
+ * ❌ return ctx.deny(`Blocked: ${cmd}`);
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DenyIfTouchesSecretsPlugin = void 0;
16
+ const hooks_1 = require("@crewx/sdk/hooks");
17
+ const SECRET_PATTERNS = ['.env', 'credentials.json', 'service-account-key.json'];
18
+ const SHELL_TOOL_NAMES = new Set(['Bash', 'shell', 'local_shell']);
19
+ class DenyIfTouchesSecretsPlugin extends hooks_1.HookPlugin {
20
+ name = 'deny-secrets';
21
+ version = '0.1.0';
22
+ capabilities = { required: ['deny'] };
23
+ async run(ctx) {
24
+ if (!SHELL_TOOL_NAMES.has(ctx.tool.rawName) && ctx.tool.name !== 'shell') {
25
+ return ctx.pass();
26
+ }
27
+ const input = ctx.tool.input;
28
+ const command = input?.command;
29
+ if (typeof command !== 'string') {
30
+ return ctx.pass();
31
+ }
32
+ for (const pattern of SECRET_PATTERNS) {
33
+ if (command.includes(pattern)) {
34
+ return ctx.deny('Secrets-related command');
35
+ }
36
+ }
37
+ return ctx.pass();
38
+ }
39
+ }
40
+ exports.DenyIfTouchesSecretsPlugin = DenyIfTouchesSecretsPlugin;
package/dist/main.js CHANGED
@@ -64,31 +64,47 @@ const install_1 = require("./commands/hook/install");
64
64
  const uninstall_1 = require("./commands/hook/uninstall");
65
65
  const status_1 = require("./commands/hook/status");
66
66
  const hook_dispatch_1 = require("./commands/hook-dispatch");
67
+ const parse_common_flags_1 = require("./commands/parse-common-flags");
68
+ const registry_1 = require("./commands/registry");
67
69
  const version_1 = require("./utils/version");
68
- // Commands deferred to future rounds
69
- const NOT_YET_MIGRATED = new Set([
70
- 'template',
71
- 'templates',
72
- 'chat',
73
- 'mcp',
74
- ]);
70
+ /**
71
+ * Dev-only SSOT assertion: bin/cli-commands.js must be the exact union of
72
+ * KNOWN_COMMANDS ∪ BUILTIN_COMMAND_NAMES ∪ NOT_YET_MIGRATED from registry.ts.
73
+ * Throws on drift so the discrepancy is caught immediately during development.
74
+ */
75
+ async function assertSsotParity() {
76
+ const { join } = await Promise.resolve().then(() => __importStar(require('path')));
77
+ const { createRequire } = await Promise.resolve().then(() => __importStar(require('module')));
78
+ // Use createRequire(__filename) + plain path to avoid TypeScript compiling import() → require()
79
+ // in the CJS dist. Node.js 22.12+ supports require(esm), so this loads ESM cli-commands.js fine.
80
+ const req = createRequire(__filename);
81
+ const cliCommandsPath = join(__dirname, '../../../bin/cli-commands.js');
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ const { CLI_SUBCOMMANDS } = req(cliCommandsPath);
84
+ const union = new Set([...registry_1.KNOWN_COMMANDS, ...registry_1.BUILTIN_COMMAND_NAMES, ...registry_1.NOT_YET_MIGRATED]);
85
+ const missing = [...union].filter((c) => !CLI_SUBCOMMANDS.has(c));
86
+ const extra = [...CLI_SUBCOMMANDS].filter((c) => !union.has(c));
87
+ if (missing.length > 0 || extra.length > 0) {
88
+ throw new Error(`[crewx dev] SSOT drift detected!\n` +
89
+ (missing.length ? ` registry has, CLI_SUBCOMMANDS missing: ${missing.join(', ')}\n` : '') +
90
+ (extra.length ? ` CLI_SUBCOMMANDS has, registry missing: ${extra.join(', ')}` : ''));
91
+ }
92
+ }
75
93
  async function main() {
94
+ if (process.env['NODE_ENV'] === 'development') {
95
+ await assertSsotParity();
96
+ }
76
97
  const args = process.argv.slice(2);
77
98
  const command = args[0];
78
99
  if (!command) {
79
100
  printHelp();
80
101
  process.exit(0);
81
102
  }
82
- // Built-in tools: memory, search, doc, wbs, cron, workflow
103
+ // Built-in tools: memory, search, doc, wbs, cron, workflow, skill
83
104
  if (builtin_1.BUILTIN_COMMANDS.has(command)) {
84
105
  await (0, builtin_1.handleBuiltin)(command, args.slice(1));
85
106
  return;
86
107
  }
87
- // P2: skill routing — pass through to @crewx/skill binary
88
- if (command === 'skill') {
89
- await runSkill(args.slice(1));
90
- return;
91
- }
92
108
  // Core commands
93
109
  switch (command) {
94
110
  // P0-2: q/x aliases
@@ -169,7 +185,7 @@ async function main() {
169
185
  }
170
186
  }
171
187
  // Not yet migrated commands (SDK-009)
172
- if (NOT_YET_MIGRATED.has(command)) {
188
+ if (registry_1.NOT_YET_MIGRATED.has(command)) {
173
189
  console.error(`Command '${command}' is not yet migrated in SDK refactor round. ` +
174
190
  'See packages/cli-bak for reference.');
175
191
  process.exit(1);
@@ -199,103 +215,81 @@ async function main() {
199
215
  console.error('Run `crewx --help` for usage.');
200
216
  process.exit(1);
201
217
  }
202
- /**
203
- * P2: Route `crewx skill ...` to the @crewx/skill CLI.
204
- * Uses `node <skill-cli-path>` to avoid symlink exec permission issues.
205
- * Falls back to spawning 'skill' on PATH if the local binary is not found.
206
- */
207
- async function runSkill(skillArgs) {
208
- const { spawn } = await Promise.resolve().then(() => __importStar(require('child_process')));
209
- const { resolve } = await Promise.resolve().then(() => __importStar(require('path')));
210
- const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs')));
211
- // Resolve the @crewx/skill CLI entry point directly (monorepo root)
212
- const skillCliJs = resolve(__dirname, '../../../node_modules/@crewx/skill/dist/cli.js');
213
- await new Promise((res, rej) => {
214
- // Prefer: node <skill-cli.js> (avoids symlink exec issues on macOS)
215
- const [cmd, args] = existsSync(skillCliJs)
216
- ? ['node', [skillCliJs, ...skillArgs]]
217
- : ['skill', skillArgs];
218
- const proc = spawn(cmd, args, { stdio: 'inherit' });
219
- proc.on('close', (code) => {
220
- process.exitCode = code ?? 0;
221
- res();
222
- });
223
- proc.on('error', (spawnErr) => {
224
- if (cmd === 'node') {
225
- // Fallback: try 'skill' on PATH
226
- const fallback = spawn('skill', skillArgs, { stdio: 'inherit' });
227
- fallback.on('close', (c) => { process.exitCode = c ?? 0; res(); });
228
- fallback.on('error', rej);
229
- }
230
- else {
231
- rej(spawnErr);
232
- }
233
- });
234
- });
235
- }
236
218
  function printHelp() {
237
- console.log(`
238
- CrewX CLI v${version_1.CLI_VERSION}
239
-
240
- Usage:
241
- crewx <command> [options]
242
-
243
- Query / Execute:
244
- q|query [@agent] <message> Query an agent (read-only)
245
- x|execute [@agent] <task> Execute a task with an agent (write-capable)
246
-
247
- @agent is optional — defaults to @crewx when omitted.
248
-
249
- Common flags:
250
- --thread <name> Conversation thread
251
- --provider <cli/xxx> Provider override
252
- --metadata <json> Extra metadata (JSON object, double-quoted). Propagated to events/hooks/tracing.
253
- Invalid JSON aborts with exit code 2.
254
- e.g. --metadata='{"workflow_id":"wf-1"}'
255
- --verbose Debug output mode (default: raw response only)
256
- --config/-c <path> Config file path (default: CREWX_CONFIG or crewx.yaml)
257
- --output-format <fmt> Output format (json|text|stream-json)
258
- --effort <level> Model effort (high|medium|low)
259
-
260
- Agent Management:
261
- agent ls [options] List configured agents
262
- --role <value> Filter by role (comma-separated for OR match)
263
- --team <value> Filter by team (comma-separated for OR match)
264
- --provider <value> Filter by provider (comma-separated for OR match)
265
- agent prompt <@id> Show rendered system prompt for an agent
266
-
267
- Task Management:
268
- ps List running tasks
269
- kill <task-id> Kill a running task
270
- kill --all Kill all running tasks
271
- result [task-id] Get task result (or list recent tasks)
272
-
273
- Logs & Diagnostics:
274
- log [ls|<task-id>] View task logs
275
- doctor [--config <path>] Run system diagnosis
276
- init [--force] [--config <p>] Initialize crewx.yaml
277
-
278
- Built-in Tools:
279
- memory <args> Memory tool
280
- search <args> Search tool
281
- doc <args> Doc tool
282
- wbs <args> WBS tool
283
- cron <args> Cron tool
284
- workflow <args> Workflow tool
285
- skill <args> Skill tool
286
-
287
- Hook Platform:
288
- hook install [--yes] Install PreToolUse hook in .claude/settings.json
289
- hook uninstall Remove crewx hook from .claude/settings.json
290
- hook status Show hook installation status and plugins
291
- hook-dispatch Internal: IPC router called by Claude (stdin→stdout)
292
-
293
- Global Options:
294
- --help, -h Show this help
295
- --version, -v Show version
219
+ console.log(`
220
+ CrewX CLI v${version_1.CLI_VERSION}
221
+
222
+ Usage:
223
+ crewx Launch web dashboard + open browser (default)
224
+ crewx <command> [options]
225
+
226
+ UI Modes:
227
+ crewx Web dashboard (port 8150, random token, auto-open)
228
+ crewx serve [options] Web server (explicit)
229
+ --port <N> Port (default: 8150)
230
+ --token <T> MCP bearer token (⚠️ use --token-file in shared shells)
231
+ --token-file <PATH> Read bearer token from file
232
+ --no-open Do not auto-open browser
233
+ crewx electron [--overlay] Launch Electron (desktop or overlay window)
234
+
235
+ Query / Execute:
236
+ q|query "@agent <message>" Query an agent (quoted single string)
237
+ x|execute "@agent <task>" Execute a task with an agent (quoted single string)
238
+
239
+ @agent is optional — defaults to @crewx when omitted.
240
+
241
+ Common flags:
242
+ --thread <name> Conversation thread
243
+ --provider <cli/xxx> Provider override
244
+ --metadata <json> Extra metadata (JSON object, double-quoted). Propagated to events/hooks/tracing.
245
+ Invalid JSON aborts with exit code 2.
246
+ e.g. --metadata='{"workflow_id":"wf-1"}'
247
+ --verbose Debug output mode (default: raw response only)
248
+ --config/-c <path> Config file path (default: CREWX_CONFIG or crewx.yaml)
249
+ --output-format <fmt> Output format (json|text|stream-json)
250
+ --effort <level> Model effort (high|medium|low)
251
+ -- End of flags; remaining tokens treated as message text
252
+ e.g. crewx q "@agent label" -- --flag-in-message
253
+
254
+ Agent Management:
255
+ agent ls [options] List configured agents
256
+ --role <value> Filter by role (comma-separated for OR match)
257
+ --team <value> Filter by team (comma-separated for OR match)
258
+ --provider <value> Filter by provider (comma-separated for OR match)
259
+ agent prompt <@id> Show rendered system prompt for an agent
260
+
261
+ Task Management:
262
+ ps List running tasks
263
+ kill <task-id> Kill a running task
264
+ kill --all Kill all running tasks
265
+ result [task-id] Get task result (or list recent tasks)
266
+
267
+ Logs & Diagnostics:
268
+ log [ls|<task-id>] View task logs
269
+ doctor [--config <path>] Run system diagnosis
270
+ init [--force] [--config <p>] Initialize crewx.yaml
271
+
272
+ Built-in Tools:
273
+ memory <args> Memory tool
274
+ search <args> Search tool
275
+ doc <args> Doc tool
276
+ wbs <args> WBS tool
277
+ cron <args> Cron tool
278
+ workflow <args> Workflow tool
279
+ skill <args> Skill tool
280
+
281
+ Hook Platform:
282
+ hook install [--yes] Install PreToolUse hook in .claude/settings.json
283
+ hook uninstall Remove crewx hook from .claude/settings.json
284
+ hook status Show hook installation status and plugins
285
+ hook-dispatch Internal: IPC router called by Claude (stdin→stdout)
286
+
287
+ Global Options:
288
+ --help, -h Show this help
289
+ --version, -v Show version
296
290
  `.trim());
297
291
  }
298
292
  main().catch((err) => {
299
293
  console.error(err instanceof Error ? err.message : String(err));
300
- process.exit(1);
294
+ process.exit(err instanceof parse_common_flags_1.UnknownOptionError ? 2 : 1);
301
295
  });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * EchoObserverPlugin — Tool observer that echoes events to a JSONL log.
3
+ *
4
+ * Observes tool:before / tool:after events from the Crewx event bus
5
+ * and appends them as JSONL to ~/.crewx/logs/echo-hook.log.
6
+ * Pure observer — no flow control (deny/inject/modify not applicable).
7
+ */
8
+ import { ToolObserverPlugin } from '@crewx/sdk/hooks';
9
+ import type { ObserverContext, ObserverResult } from '@crewx/sdk/hooks';
10
+ export declare class EchoHookPlugin extends ToolObserverPlugin {
11
+ readonly name = "echo-hook";
12
+ readonly version = "0.0.1";
13
+ readonly on: {
14
+ beforeTool: true;
15
+ afterTool: true;
16
+ beforePrompt: true;
17
+ sessionStart: true;
18
+ };
19
+ private readonly logPath;
20
+ constructor(logDir?: string);
21
+ run(ctx: ObserverContext): Promise<ObserverResult>;
22
+ private ensureLogDir;
23
+ private echo;
24
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * EchoObserverPlugin — Tool observer that echoes events to a JSONL log.
4
+ *
5
+ * Observes tool:before / tool:after events from the Crewx event bus
6
+ * and appends them as JSONL to ~/.crewx/logs/echo-hook.log.
7
+ * Pure observer — no flow control (deny/inject/modify not applicable).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.EchoHookPlugin = void 0;
11
+ const fs_1 = require("fs");
12
+ const path_1 = require("path");
13
+ const os_1 = require("os");
14
+ const hooks_1 = require("@crewx/sdk/hooks");
15
+ class EchoHookPlugin extends hooks_1.ToolObserverPlugin {
16
+ name = 'echo-hook';
17
+ version = '0.0.1';
18
+ on = {
19
+ beforeTool: true,
20
+ afterTool: true,
21
+ beforePrompt: true,
22
+ sessionStart: true,
23
+ };
24
+ logPath;
25
+ constructor(logDir) {
26
+ super();
27
+ this.logPath = (0, path_1.join)(logDir ?? (0, os_1.homedir)(), '.crewx', 'logs', 'echo-hook.log');
28
+ this.ensureLogDir();
29
+ }
30
+ async run(ctx) {
31
+ this.echo(ctx);
32
+ return ctx.pass();
33
+ }
34
+ ensureLogDir() {
35
+ const dir = (0, path_1.dirname)(this.logPath);
36
+ if (!(0, fs_1.existsSync)(dir)) {
37
+ (0, fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
38
+ }
39
+ }
40
+ echo(ctx) {
41
+ try {
42
+ const line = JSON.stringify({
43
+ timestamp: new Date().toISOString(),
44
+ event: ctx.event,
45
+ traceId: ctx.traceId,
46
+ agent: ctx.agent,
47
+ provider: ctx.provider,
48
+ thread: ctx.thread,
49
+ tool: ctx.tool,
50
+ cwd: ctx.cwd,
51
+ sessionId: ctx.sessionId,
52
+ });
53
+ (0, fs_1.appendFileSync)(this.logPath, line + '\n', { encoding: 'utf8', mode: 0o600 });
54
+ }
55
+ catch {
56
+ // Non-fatal
57
+ }
58
+ }
59
+ }
60
+ exports.EchoHookPlugin = EchoHookPlugin;
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Phase 0 e2e verification — EchoHookPlugin logs Claude Code tool calls.
4
+ *
5
+ * Run: npx tsx packages/cli/src/plugins/examples/verify-echo-hook.ts
6
+ * Prereq: claude CLI installed, crewx.yaml with @claude agent
7
+ */
8
+ export {};
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env npx tsx
2
+ "use strict";
3
+ /**
4
+ * Phase 0 e2e verification — EchoHookPlugin logs Claude Code tool calls.
5
+ *
6
+ * Run: npx tsx packages/cli/src/plugins/examples/verify-echo-hook.ts
7
+ * Prereq: claude CLI installed, crewx.yaml with @claude agent
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ const sdk_1 = require("@crewx/sdk");
11
+ const echo_hook_1 = require("./echo-hook");
12
+ const fs_1 = require("fs");
13
+ const path_1 = require("path");
14
+ const os_1 = require("os");
15
+ const LOG_PATH = (0, path_1.join)((0, os_1.homedir)(), '.crewx', 'logs', 'echo-hook.log');
16
+ async function main() {
17
+ if ((0, fs_1.existsSync)(LOG_PATH))
18
+ (0, fs_1.unlinkSync)(LOG_PATH);
19
+ const crewx = await sdk_1.Crewx.loadYaml('crewx.yaml');
20
+ await crewx.use(new echo_hook_1.EchoHookPlugin());
21
+ console.log('[verify] querying @claude with "ls"...');
22
+ await crewx.query('@claude', 'Run ls in the current directory. Use the Bash tool.');
23
+ await crewx.close();
24
+ if (!(0, fs_1.existsSync)(LOG_PATH)) {
25
+ console.error('[FAIL] echo-hook.log not created');
26
+ process.exit(1);
27
+ }
28
+ const lines = (0, fs_1.readFileSync)(LOG_PATH, 'utf8').trim().split('\n');
29
+ const entries = lines.map((l) => JSON.parse(l));
30
+ const hasBeforeTool = entries.some((e) => e.event === 'beforeTool');
31
+ const hasTraceId = entries.every((e) => e.traceId?.startsWith('tsk_'));
32
+ const hasToolName = entries
33
+ .filter((e) => e.event === 'beforeTool')
34
+ .every((e) => typeof e.tool?.rawName === 'string' && e.tool.rawName.length > 0);
35
+ console.log(`[verify] entries: ${entries.length}`);
36
+ console.log(`[verify] beforeTool: ${hasBeforeTool}`);
37
+ console.log(`[verify] traceId: ${hasTraceId}`);
38
+ console.log(`[verify] toolName: ${hasToolName}`);
39
+ if (hasBeforeTool && hasTraceId && hasToolName) {
40
+ console.log('[PASS] Phase 0 e2e verification passed');
41
+ }
42
+ else {
43
+ console.error('[FAIL] Some checks failed');
44
+ process.exit(1);
45
+ }
46
+ }
47
+ main().catch((err) => { console.error(err); process.exit(1); });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Re-export of SqliteTracingPlugin with a CLI-friendly constructor that
3
+ * accepts a plain dbRoot string (in addition to the SDK's options-object form).
4
+ */
5
+ import { SqliteTracingPlugin as SdkSqliteTracingPlugin } from '@crewx/sdk/plugins';
6
+ export declare class SqliteTracingPlugin extends SdkSqliteTracingPlugin {
7
+ constructor(dbRootOrOpts?: string | {
8
+ dbRoot?: string;
9
+ version?: string;
10
+ });
11
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SqliteTracingPlugin = void 0;
4
+ /**
5
+ * Re-export of SqliteTracingPlugin with a CLI-friendly constructor that
6
+ * accepts a plain dbRoot string (in addition to the SDK's options-object form).
7
+ */
8
+ const plugins_1 = require("@crewx/sdk/plugins");
9
+ class SqliteTracingPlugin extends plugins_1.SqliteTracingPlugin {
10
+ constructor(dbRootOrOpts) {
11
+ if (typeof dbRootOrOpts === 'string') {
12
+ super({ dbRoot: dbRootOrOpts });
13
+ }
14
+ else {
15
+ super(dbRootOrOpts);
16
+ }
17
+ }
18
+ }
19
+ exports.SqliteTracingPlugin = SqliteTracingPlugin;
@@ -0,0 +1,7 @@
1
+ import type BetterSqlite3 from 'better-sqlite3';
2
+ /**
3
+ * Ensure the `tasks` table exists in the given database.
4
+ * Safe to call multiple times — CREATE TABLE IF NOT EXISTS is idempotent.
5
+ * Owns the canonical schema for the CLI-side tasks table.
6
+ */
7
+ export declare function ensureTasksSchema(db: InstanceType<typeof BetterSqlite3>): void;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ensureTasksSchema = ensureTasksSchema;
4
+ /**
5
+ * Ensure the `tasks` table exists in the given database.
6
+ * Safe to call multiple times — CREATE TABLE IF NOT EXISTS is idempotent.
7
+ * Owns the canonical schema for the CLI-side tasks table.
8
+ */
9
+ function ensureTasksSchema(db) {
10
+ db.exec(`
11
+ CREATE TABLE IF NOT EXISTS tasks (
12
+ id TEXT PRIMARY KEY,
13
+ agent_id TEXT,
14
+ user_id TEXT,
15
+ prompt TEXT,
16
+ mode TEXT,
17
+ status TEXT NOT NULL,
18
+ result TEXT,
19
+ error TEXT,
20
+ started_at TEXT NOT NULL,
21
+ completed_at TEXT,
22
+ duration_ms INTEGER,
23
+ metadata TEXT,
24
+ project_id TEXT,
25
+ project_name TEXT,
26
+ trace_id TEXT,
27
+ parent_task_id TEXT,
28
+ caller_agent_id TEXT,
29
+ model TEXT,
30
+ platform TEXT,
31
+ crewx_version TEXT,
32
+ input_tokens INTEGER DEFAULT 0,
33
+ output_tokens INTEGER DEFAULT 0,
34
+ cost_usd REAL,
35
+ pid INTEGER,
36
+ rendered_prompt TEXT,
37
+ command TEXT,
38
+ coding_agent_command TEXT,
39
+ exit_code INTEGER,
40
+ logs TEXT,
41
+ thread_id TEXT,
42
+ project_ref TEXT,
43
+ workspace_id TEXT,
44
+ workspace_name TEXT,
45
+ cached_input_tokens INTEGER DEFAULT 0
46
+ )
47
+ `);
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewx/cli",
3
- "version": "0.8.1-rc.4",
3
+ "version": "0.8.2-rc.1",
4
4
  "license": "UNLICENSED",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -25,17 +25,17 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@crewx/adapter-slack": "0.1.3",
28
+ "@crewx/adapter-slack": "^0.1.4",
29
29
  "better-sqlite3": "*",
30
- "@crewx/search": "0.1.8",
31
- "@crewx/sdk": "0.8.1-rc.4",
30
+ "@crewx/sdk": "0.8.2-rc.1",
32
31
  "@crewx/memory": "0.1.9",
33
- "@crewx/wbs": "0.1.8",
34
32
  "@crewx/doc": "0.1.7",
35
- "@crewx/skill": "0.1.6",
36
- "@crewx/workflow": "0.3.7",
37
- "@crewx/shared": "0.0.4",
38
- "@crewx/cron": "0.1.7"
33
+ "@crewx/search": "0.1.8",
34
+ "@crewx/wbs": "0.1.8",
35
+ "@crewx/skill": "0.1.7",
36
+ "@crewx/cron": "0.1.7",
37
+ "@crewx/workflow": "0.3.8",
38
+ "@crewx/shared": "0.0.4"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/better-sqlite3": "*",