openstoat 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * OpenStoat CLI - Agent super-manual for task orchestration
4
+ * CLI is the operational guide for both agents and humans.
5
+ */
6
+ import yargs from 'yargs';
7
+ import { hideBin } from 'yargs/helpers';
8
+ import { projectCommands } from './commands/project.js';
9
+ import { taskCommands } from './commands/task.js';
10
+ import { daemonCommands } from './commands/daemon.js';
11
+ import { installCommands } from './commands/install.js';
12
+ import { AGENT_MANUAL } from './agent-manual.js';
13
+ const cli = yargs(hideBin(process.argv))
14
+ .scriptName('openstoat')
15
+ .usage('Usage: $0 <command> [options]\n\n' +
16
+ 'OpenStoat orchestrates tasks between AI agents and humans. Local-first, CLI-first.\n' +
17
+ 'Storage: ~/.openstoat/ (SQLite). No account/API Key required.\n\n' +
18
+ 'WHEN TO USE: Planning work, creating tasks, claiming/executing tasks, handing off to humans,\n' +
19
+ 'self-unblocking when blocked. Run \'openstoat manual\' for full agent operational manual.\n\n' +
20
+ 'Commands:\n' +
21
+ ' project init Initialize project (--id, --name, --template required)\n' +
22
+ ' project ls List projects (get --project IDs for task commands)\n' +
23
+ ' project show <id> Show project details\n' +
24
+ ' task ls List tasks (PLANNER: run before create to avoid duplicates)\n' +
25
+ ' task create Create task (all required: --project, --title, --description,\n' +
26
+ ' --acceptance-criteria, --status, --owner, --task-type)\n' +
27
+ ' task claim <id> WORKER: claim ready task (--as agent_worker|human_worker)\n' +
28
+ ' task start <id> WORKER: start working (append to logs)\n' +
29
+ ' task done <id> WORKER: complete (--output, --handoff-summary min 200 chars)\n' +
30
+ ' task self-unblock WORKER: rollback when blocked (--depends-on human_task required)\n' +
31
+ ' task show <id> Show task details\n' +
32
+ ' daemon init Interactively create .openstoat.json\n' +
33
+ ' daemon start Start worker daemon (polls for ready agent_worker tasks)\n' +
34
+ ' install skill Install planner and worker skills (--here for current dir)\n' +
35
+ ' web Start Web UI server (http://localhost:3080)\n' +
36
+ ' manual Print full agent operational manual (SKILL-style)')
37
+ .command({
38
+ command: 'web',
39
+ describe: 'Start Web UI server. Opens http://localhost:3080 (set PORT env to change).',
40
+ handler: async () => {
41
+ await import('openstoat-web');
42
+ },
43
+ })
44
+ .command({
45
+ command: 'manual',
46
+ describe: 'Print full agent operational manual. Use when agent needs detailed workflow, rules, and examples.',
47
+ handler: () => {
48
+ console.log(AGENT_MANUAL.trim());
49
+ },
50
+ })
51
+ .command(projectCommands)
52
+ .command(taskCommands)
53
+ .command(daemonCommands)
54
+ .command(installCommands)
55
+ .demandCommand(1, 'Specify a command. Run openstoat --help or openstoat manual for details.')
56
+ .strict()
57
+ .help()
58
+ .alias('h', 'help')
59
+ .version('0.1.0')
60
+ .alias('v', 'version')
61
+ .epilog('RULES: (1) Planner must run task ls before create. (2) Handoff required on every done (min 200 chars). ' +
62
+ '(3) No generic status update; use claim/start/done/self-unblock only. (4) Self-unblock requires --depends-on.');
63
+ cli.parse();
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Install OpenStoat skills to .agent/skills and .claude/skills in the target directory.
3
+ * Used by `openstoat install skill` and when daemon starts.
4
+ */
5
+ export interface InstallSkillsOptions {
6
+ /** When true, install to current directory (no skills/, .agent, or .claude) */
7
+ here?: boolean;
8
+ }
9
+ /**
10
+ * Install OpenStoat skills to .agent/skills and .claude/skills, or current dir when here=true.
11
+ * @param targetRoot - Root directory (default: process.cwd())
12
+ * @param options - { here: true } to install to current directory (./openstoat-planner, ./openstoat-worker)
13
+ * @returns Paths where skills were installed
14
+ */
15
+ export declare function installSkills(targetRoot?: string, options?: InstallSkillsOptions): string[];
16
+ //# sourceMappingURL=installSkills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installSkills.d.ts","sourceRoot":"","sources":["../../src/lib/installSkills.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAsCH,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,UAAU,SAAgB,EAC1B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,MAAM,EAAE,CAoBV"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Install OpenStoat skills to .agent/skills and .claude/skills in the target directory.
3
+ * Used by `openstoat install skill` and when daemon starts.
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { createRequire } from 'module';
8
+ import { fileURLToPath } from 'url';
9
+ const require = createRequire(import.meta.url);
10
+ const SKILL_NAMES = ['openstoat-planner', 'openstoat-worker'];
11
+ const TARGET_DIRS_DEFAULT = ['.agent/skills', '.claude/skills'];
12
+ const TARGET_DIRS_HERE = [''];
13
+ /**
14
+ * Get the path to the openstoat-skills package skills directory.
15
+ */
16
+ function getSkillsSourcePath() {
17
+ try {
18
+ // Resolve from openstoat-cli's node_modules (workspace or hoisted)
19
+ const pkgPath = require.resolve('openstoat-skills/package.json', {
20
+ paths: [path.dirname(fileURLToPath(import.meta.url))],
21
+ });
22
+ return path.join(path.dirname(pkgPath), 'skills');
23
+ }
24
+ catch {
25
+ // Fallback: relative to this file (e.g. in monorepo packages/openstoat-cli)
26
+ const dir = path.dirname(fileURLToPath(import.meta.url));
27
+ return path.join(dir, '../../openstoat-skills/skills');
28
+ }
29
+ }
30
+ /**
31
+ * Copy a skill directory recursively.
32
+ */
33
+ function copySkill(source, dest) {
34
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
35
+ fs.cpSync(source, dest, { recursive: true });
36
+ }
37
+ /**
38
+ * Install OpenStoat skills to .agent/skills and .claude/skills, or current dir when here=true.
39
+ * @param targetRoot - Root directory (default: process.cwd())
40
+ * @param options - { here: true } to install to current directory (./openstoat-planner, ./openstoat-worker)
41
+ * @returns Paths where skills were installed
42
+ */
43
+ export function installSkills(targetRoot = process.cwd(), options) {
44
+ const sourcePath = getSkillsSourcePath();
45
+ const installed = [];
46
+ const targetDirs = options?.here ? TARGET_DIRS_HERE : TARGET_DIRS_DEFAULT;
47
+ for (const skillName of SKILL_NAMES) {
48
+ const skillSource = path.join(sourcePath, skillName);
49
+ if (!fs.existsSync(skillSource)) {
50
+ console.warn(`Skill not found: ${skillName} at ${skillSource}`);
51
+ continue;
52
+ }
53
+ for (const targetDir of targetDirs) {
54
+ const dest = path.join(targetRoot, targetDir, skillName);
55
+ copySkill(skillSource, dest);
56
+ installed.push(dest);
57
+ }
58
+ }
59
+ return installed;
60
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Interactive prompt helper using readline.
3
+ */
4
+ export declare function prompt(question: string, defaultValue?: string): Promise<string>;
5
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/lib/prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,SAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAU3E"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Interactive prompt helper using readline.
3
+ */
4
+ import { createInterface } from 'readline';
5
+ export function prompt(question, defaultValue = '') {
6
+ return new Promise((resolve) => {
7
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
8
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
9
+ rl.question(`${question}${suffix}: `, (answer) => {
10
+ rl.close();
11
+ const trimmed = answer.trim();
12
+ resolve(trimmed || defaultValue);
13
+ });
14
+ });
15
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "openstoat",
3
+ "version": "0.1.0",
4
+ "bin": {
5
+ "openstoat": "./bin/openstoat"
6
+ },
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "clean": "rm -rf dist",
10
+ "start": "bun run src/index.ts"
11
+ },
12
+ "dependencies": {
13
+ "openstoat-core": "workspace:*",
14
+ "openstoat-daemon": "workspace:*",
15
+ "openstoat-skills": "workspace:*",
16
+ "openstoat-types": "workspace:*",
17
+ "openstoat-web": "workspace:*",
18
+ "yargs": "^17.7.2"
19
+ },
20
+ "devDependencies": {
21
+ "@types/bun": "latest",
22
+ "@types/node": "^20.0.0",
23
+ "@types/yargs": "^17.0.35",
24
+ "typescript": "^5.0.0"
25
+ }
26
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * OpenStoat Agent Manual - SKILL-style documentation for AI agents
3
+ * Use when: orchestrating tasks, planning work, executing tasks, or handing off to humans.
4
+ */
5
+
6
+ export const AGENT_MANUAL = `
7
+ # OpenStoat CLI - Agent Operational Manual
8
+
9
+ ## When to Use
10
+
11
+ Use OpenStoat when:
12
+ - Planning and breaking down work into executable tasks
13
+ - Creating tasks for agents or humans to execute
14
+ - Claiming and executing tasks as an agent worker
15
+ - Handing off work to humans (credentials, review, deploy)
16
+ - Self-unblocking when blocked by human input
17
+ - Querying task status, dependencies, or project context
18
+
19
+ ## Core Concepts
20
+
21
+ | Concept | Description |
22
+ |---------|-------------|
23
+ | **Project** | Template-bound workspace. Every task belongs to one project. Get project ID via \`project ls\`. |
24
+ | **Task** | Atomic work unit. Status: ready → in_progress → done. Owner: agent_worker | human_worker. |
25
+ | **Handoff** | Required on every completion. Min 200 chars. Transfers context to downstream tasks. |
26
+ | **Self-unblock** | When agent is blocked by human: create human task, then run self-unblock with --depends-on. |
27
+
28
+ ## Role-Based Workflows
29
+
30
+ ### Agent Planner
31
+
32
+ 1. **Before creating tasks** (mandatory): List existing tasks to avoid duplicates.
33
+ \`openstoat task ls --project <project_id> --status ready,in_progress\`
34
+
35
+ 2. **Create tasks**: Use \`task create\` with all required fields. Set owner=agent_worker for agent tasks, owner=human_worker for human tasks (credentials, review, deploy).
36
+
37
+ 3. **Dependencies**: Use --depends-on when task B waits for task A. Omit when no dependencies.
38
+
39
+ ### Agent Worker
40
+
41
+ 1. **Claim**: \`openstoat task claim <task_id> --as agent_worker --logs-append "..."\`
42
+ 2. **Start**: \`openstoat task start <task_id> --as agent_worker --logs-append "..."\`
43
+ 3. **Done**: \`openstoat task done <task_id> --output "..." --handoff-summary "..." --logs-append "..."\`
44
+ - handoff-summary: min 200 chars. Describe what was done and context for downstream.
45
+
46
+ ### Agent Worker (Blocked by Human)
47
+
48
+ 1. Create human task: \`openstoat task create --project X --owner human_worker --task-type credentials ...\`
49
+ 2. Self-unblock: \`openstoat task self-unblock <my_task_id> --depends-on <human_task_id> --logs-append "Blocked: ..."\`
50
+ 3. Task moves in_progress → ready. Resume after human completes the dependency.
51
+
52
+ ### Human Worker
53
+
54
+ Same flow as Agent Worker but use --as human_worker when claiming/starting/done.
55
+
56
+ ---
57
+
58
+ ## Command Reference
59
+
60
+ ### project init
61
+ Initialize a project with bound template. Required before creating tasks.
62
+ --id (required): Project ID. Use this as --project in task commands.
63
+ --name (required): Display name.
64
+ --template (required): Template name (e.g. checkout-default-v1).
65
+
66
+ ### project ls
67
+ List all projects. Output: id, name, status.
68
+
69
+ ### project show <project_id>
70
+ Show project details (JSON).
71
+
72
+ ### task ls
73
+ List tasks. **Planner must run before create to avoid duplicates.**
74
+ --project (required): Project ID.
75
+ --status: Filter. Comma-separated: ready,in_progress,done.
76
+ --owner: Filter: agent_worker | human_worker.
77
+ --json: Output as JSON.
78
+
79
+ ### task create
80
+ Create a task. All fields required except --depends-on, --created-by.
81
+ --project (required)
82
+ --title (required)
83
+ --description (required)
84
+ --acceptance-criteria (required, pass multiple): e.g. --acceptance-criteria "A" --acceptance-criteria "B"
85
+ --depends-on (optional, pass multiple): Task IDs this task depends on.
86
+ --status (required): ready | in_progress | done. New tasks typically ready.
87
+ --owner (required): agent_worker | human_worker
88
+ --task-type (required): implementation | testing | review | credentials | deploy | docs | custom
89
+ --created-by (optional): e.g. agent_planner
90
+
91
+ ### task claim <task_id>
92
+ Worker claims a ready task. Moves ready → in_progress.
93
+ --as (required): agent_worker | human_worker
94
+ --logs-append (recommended for agents): Append to task logs.
95
+
96
+ ### task start <task_id>
97
+ Worker starts working. Use after claim or to append progress.
98
+ --as (required): agent_worker | human_worker
99
+ --logs-append (optional): Append to task logs.
100
+
101
+ ### task done <task_id>
102
+ Worker completes task. Moves in_progress → done. Handoff mandatory.
103
+ --output (required): What was delivered.
104
+ --handoff-summary (required, min 200 chars): Context for downstream tasks.
105
+ --logs-append (optional)
106
+ --as (required): agent_worker | human_worker
107
+
108
+ ### task self-unblock <task_id>
109
+ Rollback in_progress → ready when blocked by human. Must add new --depends-on.
110
+ --depends-on (required, pass multiple): New human task ID(s). At least one.
111
+ --logs-append (optional)
112
+ --as: agent_worker only (default)
113
+
114
+ ### task show <task_id>
115
+ Show task details. Use --json for machine-readable output.
116
+
117
+ ### daemon init
118
+ Interactively create \`.openstoat.json\` (project, agent) in current directory.
119
+
120
+ ### daemon start
121
+ Start worker daemon. Polls for ready agent_worker tasks and invokes external agents.
122
+ --poll-interval (default 5000): Poll interval in ms.
123
+ Config: read from \`.openstoat.json\` in current directory. Example: \`{ "project": "<project_id>", "agent": "/path/to/agent" }\`
124
+
125
+ ### daemon stop | status | logs
126
+ Stop, check status, or view daemon logs.
127
+
128
+ ---
129
+
130
+ ## Rules (Do Not Violate)
131
+
132
+ 1. **No generic status update**: There is no \`task update --status\`. Use claim, start, done, self-unblock only.
133
+ 2. **Planner duplicate check**: Always run \`task ls\` before \`task create\` to avoid duplicates.
134
+ 3. **Handoff required**: Every \`task done\` must include --handoff-summary (min 200 chars).
135
+ 4. **Self-unblock guard**: in_progress → ready only via self-unblock, and only with new --depends-on.
136
+ 5. **Logs for agents**: Agent workers should use --logs-append on claim, start, done for audit trail.
137
+ 6. **Project required**: Every task has --project. Get project IDs from \`project ls\`.
138
+
139
+ ---
140
+
141
+ ## Examples
142
+
143
+ # Planner: list then create
144
+ openstoat task ls --project proj_1 --status ready,in_progress
145
+ openstoat task create --project proj_1 --title "Add Paddle mapping" --description "..." \\
146
+ --acceptance-criteria "Mapping works" --acceptance-criteria "Tests pass" \\
147
+ --status ready --owner agent_worker --task-type implementation
148
+
149
+ # Worker: full lifecycle
150
+ openstoat task claim task_001 --as agent_worker --logs-append "Claimed"
151
+ openstoat task start task_001 --as agent_worker --logs-append "Started"
152
+ openstoat task done task_001 --output "Implemented" \\
153
+ --handoff-summary "Implemented Paddle mapping in src/payments/provider-map.ts. No migration. Integration tests pass. Downstream can use PaddleProvider class." \\
154
+ --as agent_worker --logs-append "Done"
155
+
156
+ # Self-unblock (agent blocked, needs human)
157
+ openstoat task create --project proj_1 --title "Provide API key" --description "..." \\
158
+ --acceptance-criteria "Key delivered" --status ready --owner human_worker --task-type credentials
159
+ openstoat task self-unblock task_X --depends-on task_H --logs-append "Blocked: need API key"
160
+ `;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Daemon commands: start, stop, status, logs, init
3
+ * Worker daemon polls for ready agent_worker tasks and invokes external agents.
4
+ */
5
+
6
+ import type { Argv } from 'yargs';
7
+ import { startDaemon } from 'openstoat-daemon';
8
+ import { listProjects } from 'openstoat-core';
9
+ import { loadProjectConfig, saveProjectConfig } from 'openstoat-core';
10
+ import { installSkills } from '../lib/installSkills.js';
11
+ import { prompt } from '../lib/prompt.js';
12
+
13
+ export const daemonCommands = {
14
+ command: 'daemon <action>',
15
+ describe: 'Worker daemon. Polls for ready agent_worker tasks and invokes external agents.',
16
+ builder: (yargs: Argv) =>
17
+ yargs
18
+ .command({
19
+ command: 'init',
20
+ describe: 'Interactively create .openstoat.json (project, agent) in current directory.',
21
+ handler: async () => {
22
+ const existing = loadProjectConfig();
23
+ const projects = listProjects();
24
+
25
+ console.log('Initialize OpenStoat daemon config (.openstoat.json)\n');
26
+
27
+ let project = existing?.project ?? '';
28
+ if (projects.length > 0) {
29
+ console.log('Existing projects:');
30
+ for (const p of projects) {
31
+ console.log(` ${p.id}\t${p.name}`);
32
+ }
33
+ project = await prompt('Project ID', project);
34
+ } else {
35
+ console.log('No projects yet. Run `openstoat project init` first, or enter a project ID to use later.');
36
+ project = await prompt('Project ID', project);
37
+ }
38
+
39
+ const agent = await prompt('Agent executable path (for daemon to invoke)', existing?.agent ?? '');
40
+
41
+ const config: Record<string, string> = {};
42
+ if (project) config.project = project;
43
+ if (agent) config.agent = agent;
44
+
45
+ saveProjectConfig(config);
46
+ console.log('\nCreated .openstoat.json');
47
+ console.log(JSON.stringify(config, null, 2));
48
+ },
49
+ })
50
+ .command({
51
+ command: 'start',
52
+ describe: 'Start daemon. Polls for ready agent_worker tasks, invokes configured external agent.',
53
+ builder: (y: Argv) =>
54
+ y.option('poll-interval', {
55
+ type: 'number',
56
+ default: 5000,
57
+ describe: 'Poll interval in ms (default 5000)',
58
+ }),
59
+ handler: (argv: { 'poll-interval'?: number }) => {
60
+ console.log('Daemon starting...');
61
+ installSkills(process.cwd());
62
+ const pollInterval = (argv['poll-interval'] as number) || 5000;
63
+ startDaemon(pollInterval);
64
+ },
65
+ })
66
+ .command({
67
+ command: 'stop',
68
+ describe: 'Stop daemon',
69
+ handler: () => {
70
+ console.log('Daemon stop: run daemon in foreground and use Ctrl+C. PID file not implemented in MVP.');
71
+ },
72
+ })
73
+ .command({
74
+ command: 'status',
75
+ describe: 'Check daemon status',
76
+ handler: () => {
77
+ console.log('Daemon status: not running (PID file not implemented in MVP.');
78
+ },
79
+ })
80
+ .command({
81
+ command: 'logs',
82
+ describe: 'View daemon logs',
83
+ handler: () => {
84
+ console.log('Daemon logs: stdout/stderr when running.');
85
+ },
86
+ })
87
+ .demandCommand(1, 'Specify init, start, stop, status, or logs'),
88
+ handler: () => {},
89
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Install commands: skill
3
+ * Installs OpenStoat skills to .agent/skills and .claude/skills.
4
+ */
5
+
6
+ import type { Argv } from 'yargs';
7
+ import { installSkills } from '../lib/installSkills.js';
8
+
9
+ export const installCommands = {
10
+ command: 'install <target>',
11
+ describe: 'Install OpenStoat assets. Use "skill" to install planner and worker skills.',
12
+ builder: (yargs: Argv) =>
13
+ yargs
14
+ .command({
15
+ command: 'skill',
16
+ describe:
17
+ 'Install planner and worker skills. Use --here to install to current directory (no skills/, .agent, or .claude).',
18
+ builder: (y: Argv) =>
19
+ y
20
+ .option('cwd', {
21
+ type: 'string',
22
+ default: process.cwd(),
23
+ describe: 'Target directory (default: current working directory)',
24
+ })
25
+ .option('here', {
26
+ type: 'boolean',
27
+ default: false,
28
+ describe: 'Install to current directory (./openstoat-planner, ./openstoat-worker)',
29
+ }),
30
+ handler: (argv: { cwd?: string; here?: boolean }) => {
31
+ const targetRoot = (argv.cwd as string) || process.cwd();
32
+ const installed = installSkills(targetRoot, { here: argv.here });
33
+ if (installed.length > 0) {
34
+ console.log('Installed skills to:');
35
+ for (const p of installed) {
36
+ console.log(` ${p}`);
37
+ }
38
+ } else {
39
+ console.log('No skills installed.');
40
+ }
41
+ },
42
+ })
43
+ .demandCommand(1, 'Specify skill'),
44
+ handler: () => {},
45
+ };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Project commands: init, ls, show
3
+ */
4
+
5
+ import type { Argv } from 'yargs';
6
+ import { createProject, getProject, listProjects } from 'openstoat-core';
7
+
8
+ export const projectCommands = {
9
+ command: 'project <action>',
10
+ describe: 'Manage projects. Each project is template-bound. Use project ID as --project in task commands.',
11
+ builder: (yargs: Argv) =>
12
+ yargs
13
+ .command({
14
+ command: 'init',
15
+ describe: 'Initialize project with bound template. Required before creating tasks.',
16
+ builder: (y: Argv) =>
17
+ y
18
+ .option('id', {
19
+ type: 'string',
20
+ demandOption: true,
21
+ describe: 'Project ID. Use this as --project in all task commands.',
22
+ })
23
+ .option('name', {
24
+ type: 'string',
25
+ demandOption: true,
26
+ describe: 'Display name',
27
+ })
28
+ .option('template', {
29
+ type: 'string',
30
+ demandOption: true,
31
+ describe: 'Template name (e.g. checkout-default-v1). Defines default owner per task_type.',
32
+ }),
33
+ handler: (argv: { id?: string; name?: string; template?: string; project_id?: string }) => {
34
+ const project = createProject(
35
+ argv.id as string,
36
+ argv.name as string,
37
+ argv.template as string
38
+ );
39
+ console.log(`Project initialized: ${project.id}`);
40
+ },
41
+ })
42
+ .command({
43
+ command: 'ls',
44
+ describe: 'List projects. Output: id, name, status. Get --project for task commands.',
45
+ handler: () => {
46
+ const projects = listProjects();
47
+ if (projects.length === 0) {
48
+ console.log('No projects found.');
49
+ return;
50
+ }
51
+ for (const p of projects) {
52
+ console.log(`${p.id}\t${p.name}\t${p.status}`);
53
+ }
54
+ },
55
+ })
56
+ .command({
57
+ command: 'show <project_id>',
58
+ describe: 'Show project details (JSON). Includes template_context.',
59
+ builder: (y: Argv) => y.positional('project_id', { type: 'string', demandOption: true }),
60
+ handler: (argv: { id?: string; name?: string; template?: string; project_id?: string }) => {
61
+ const project = getProject(argv.project_id as string);
62
+ if (!project) {
63
+ console.error(`Project '${argv.project_id}' not found.`);
64
+ process.exit(1);
65
+ }
66
+ console.log(JSON.stringify(project, null, 2));
67
+ },
68
+ })
69
+ .demandCommand(1, 'Specify init, ls, or show'),
70
+ handler: () => {},
71
+ };