feat: gated project experience & prompt intelligence
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / typecheck (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / package (pull_request) Has been cancelled

Implements the full gated session flow and prompt intelligence system:

- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
  tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
  describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-25 23:22:42 +00:00
parent 62647a7f90
commit 705df06996
46 changed files with 4946 additions and 105 deletions

View File

@@ -447,4 +447,114 @@ describe('create command', () => {
});
});
});
describe('create prompt', () => {
it('creates a prompt with content', async () => {
vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'test-prompt' });
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync(['prompt', 'test-prompt', '--content', 'Hello world'], { from: 'user' });
expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', {
name: 'test-prompt',
content: 'Hello world',
});
expect(output.join('\n')).toContain("prompt 'test-prompt' created");
});
it('requires content or content-file', async () => {
const cmd = createCreateCommand({ client, log });
await expect(
cmd.parseAsync(['prompt', 'no-content'], { from: 'user' }),
).rejects.toThrow('--content or --content-file is required');
});
it('--priority sets prompt priority', async () => {
vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'pri-prompt' });
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync(['prompt', 'pri-prompt', '--content', 'x', '--priority', '8'], { from: 'user' });
expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', expect.objectContaining({
priority: 8,
}));
});
it('--priority validates range 1-10', async () => {
const cmd = createCreateCommand({ client, log });
await expect(
cmd.parseAsync(['prompt', 'bad', '--content', 'x', '--priority', '15'], { from: 'user' }),
).rejects.toThrow('--priority must be a number between 1 and 10');
});
it('--priority rejects zero', async () => {
const cmd = createCreateCommand({ client, log });
await expect(
cmd.parseAsync(['prompt', 'bad', '--content', 'x', '--priority', '0'], { from: 'user' }),
).rejects.toThrow('--priority must be a number between 1 and 10');
});
it('--link sets linkTarget', async () => {
vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'linked' });
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync(['prompt', 'linked', '--content', 'x', '--link', 'proj/srv:docmost://pages/abc'], { from: 'user' });
expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', expect.objectContaining({
linkTarget: 'proj/srv:docmost://pages/abc',
}));
});
it('--project resolves project name to ID', async () => {
vi.mocked(client.get).mockResolvedValueOnce([{ id: 'proj-1', name: 'my-project' }] as never);
vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'scoped' });
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync(['prompt', 'scoped', '--content', 'x', '--project', 'my-project'], { from: 'user' });
expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', expect.objectContaining({
projectId: 'proj-1',
}));
});
it('--project throws when project not found', async () => {
vi.mocked(client.get).mockResolvedValueOnce([] as never);
const cmd = createCreateCommand({ client, log });
await expect(
cmd.parseAsync(['prompt', 'bad', '--content', 'x', '--project', 'nope'], { from: 'user' }),
).rejects.toThrow("Project 'nope' not found");
});
});
describe('create promptrequest', () => {
it('creates a prompt request with priority', async () => {
vi.mocked(client.post).mockResolvedValueOnce({ id: 'r-1', name: 'req' });
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync(['promptrequest', 'req', '--content', 'proposal', '--priority', '7'], { from: 'user' });
expect(client.post).toHaveBeenCalledWith('/api/v1/promptrequests', expect.objectContaining({
name: 'req',
content: 'proposal',
priority: 7,
}));
});
});
describe('create project', () => {
it('creates a project with --gated', async () => {
vi.mocked(client.post).mockResolvedValueOnce({ id: 'proj-1', name: 'gated-proj' });
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync(['project', 'gated-proj', '--gated'], { from: 'user' });
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
gated: true,
}));
});
it('creates a project with --no-gated', async () => {
vi.mocked(client.post).mockResolvedValueOnce({ id: 'proj-1', name: 'open-proj' });
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync(['project', 'open-proj', '--no-gated'], { from: 'user' });
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
gated: false,
}));
});
});
});

View File

@@ -20,7 +20,7 @@ describe('get command', () => {
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'servers']);
expect(deps.fetchResource).toHaveBeenCalledWith('servers', undefined);
expect(deps.fetchResource).toHaveBeenCalledWith('servers', undefined, undefined);
expect(deps.output[0]).toContain('NAME');
expect(deps.output[0]).toContain('TRANSPORT');
expect(deps.output.join('\n')).toContain('slack');
@@ -31,14 +31,14 @@ describe('get command', () => {
const deps = makeDeps([]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'srv']);
expect(deps.fetchResource).toHaveBeenCalledWith('servers', undefined);
expect(deps.fetchResource).toHaveBeenCalledWith('servers', undefined, undefined);
});
it('passes ID when provided', async () => {
const deps = makeDeps([{ id: 'srv-1', name: 'slack' }]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'servers', 'srv-1']);
expect(deps.fetchResource).toHaveBeenCalledWith('servers', 'srv-1');
expect(deps.fetchResource).toHaveBeenCalledWith('servers', 'srv-1', undefined);
});
it('outputs apply-compatible JSON format', async () => {
@@ -94,7 +94,7 @@ describe('get command', () => {
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'users']);
expect(deps.fetchResource).toHaveBeenCalledWith('users', undefined);
expect(deps.fetchResource).toHaveBeenCalledWith('users', undefined, undefined);
const text = deps.output.join('\n');
expect(text).toContain('EMAIL');
expect(text).toContain('NAME');
@@ -110,7 +110,7 @@ describe('get command', () => {
const deps = makeDeps([]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'user']);
expect(deps.fetchResource).toHaveBeenCalledWith('users', undefined);
expect(deps.fetchResource).toHaveBeenCalledWith('users', undefined, undefined);
});
it('lists groups with correct columns', async () => {
@@ -126,7 +126,7 @@ describe('get command', () => {
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'groups']);
expect(deps.fetchResource).toHaveBeenCalledWith('groups', undefined);
expect(deps.fetchResource).toHaveBeenCalledWith('groups', undefined, undefined);
const text = deps.output.join('\n');
expect(text).toContain('NAME');
expect(text).toContain('MEMBERS');
@@ -141,7 +141,7 @@ describe('get command', () => {
const deps = makeDeps([]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'group']);
expect(deps.fetchResource).toHaveBeenCalledWith('groups', undefined);
expect(deps.fetchResource).toHaveBeenCalledWith('groups', undefined, undefined);
});
it('lists rbac definitions with correct columns', async () => {
@@ -156,7 +156,7 @@ describe('get command', () => {
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'rbac']);
expect(deps.fetchResource).toHaveBeenCalledWith('rbac', undefined);
expect(deps.fetchResource).toHaveBeenCalledWith('rbac', undefined, undefined);
const text = deps.output.join('\n');
expect(text).toContain('NAME');
expect(text).toContain('SUBJECTS');
@@ -170,7 +170,7 @@ describe('get command', () => {
const deps = makeDeps([]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'rbac-definition']);
expect(deps.fetchResource).toHaveBeenCalledWith('rbac', undefined);
expect(deps.fetchResource).toHaveBeenCalledWith('rbac', undefined, undefined);
});
it('lists projects with new columns', async () => {
@@ -251,4 +251,87 @@ describe('get command', () => {
await cmd.parseAsync(['node', 'test', 'rbac']);
expect(deps.output[0]).toContain('No rbac found');
});
it('lists prompts with project name column', async () => {
const deps = makeDeps([
{ id: 'p-1', name: 'debug-guide', projectId: 'proj-1', project: { name: 'smart-home' }, createdAt: '2025-01-01T00:00:00Z' },
{ id: 'p-2', name: 'global-rules', projectId: null, project: null, createdAt: '2025-01-01T00:00:00Z' },
]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'prompts']);
const text = deps.output.join('\n');
expect(text).toContain('NAME');
expect(text).toContain('PROJECT');
expect(text).toContain('debug-guide');
expect(text).toContain('smart-home');
expect(text).toContain('global-rules');
expect(text).toContain('(global)');
});
it('lists promptrequests with project name column', async () => {
const deps = makeDeps([
{ id: 'pr-1', name: 'new-rule', projectId: 'proj-1', project: { name: 'my-project' }, createdBySession: 'sess-abc123def456', createdAt: '2025-01-01T00:00:00Z' },
]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'promptrequests']);
const text = deps.output.join('\n');
expect(text).toContain('new-rule');
expect(text).toContain('my-project');
expect(text).toContain('sess-abc123d');
});
it('passes --project option to fetchResource', async () => {
const deps = makeDeps([]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'prompts', '--project', 'smart-home']);
expect(deps.fetchResource).toHaveBeenCalledWith('prompts', undefined, { project: 'smart-home' });
});
it('does not pass project when --project is not specified', async () => {
const deps = makeDeps([]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'prompts']);
expect(deps.fetchResource).toHaveBeenCalledWith('prompts', undefined, undefined);
});
it('passes --all flag to fetchResource', async () => {
const deps = makeDeps([]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'prompts', '-A']);
expect(deps.fetchResource).toHaveBeenCalledWith('prompts', undefined, { all: true });
});
it('passes both --project and --all when both given', async () => {
const deps = makeDeps([]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'prompts', '--project', 'my-proj', '-A']);
expect(deps.fetchResource).toHaveBeenCalledWith('prompts', undefined, { project: 'my-proj', all: true });
});
it('resolves prompt alias', async () => {
const deps = makeDeps([]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'prompt']);
expect(deps.fetchResource).toHaveBeenCalledWith('prompts', undefined, undefined);
});
it('resolves pr alias to promptrequests', async () => {
const deps = makeDeps([]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'pr']);
expect(deps.fetchResource).toHaveBeenCalledWith('promptrequests', undefined, undefined);
});
it('shows no results message for empty prompts list', async () => {
const deps = makeDeps([]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'prompts']);
expect(deps.output[0]).toContain('No prompts found');
});
});

View File

@@ -47,7 +47,7 @@ describe('CLI command registration (e2e)', () => {
expect(subcommands).toContain('reset');
});
it('create command has user, group, rbac subcommands', () => {
it('create command has user, group, rbac, prompt, promptrequest subcommands', () => {
const program = createProgram();
const create = program.commands.find((c) => c.name() === 'create');
expect(create).toBeDefined();
@@ -59,6 +59,24 @@ describe('CLI command registration (e2e)', () => {
expect(subcommands).toContain('user');
expect(subcommands).toContain('group');
expect(subcommands).toContain('rbac');
expect(subcommands).toContain('prompt');
expect(subcommands).toContain('promptrequest');
});
it('get command accepts --project option', () => {
const program = createProgram();
const get = program.commands.find((c) => c.name() === 'get');
expect(get).toBeDefined();
const projectOpt = get!.options.find((o) => o.long === '--project');
expect(projectOpt).toBeDefined();
expect(projectOpt!.description).toContain('project');
});
it('program-level --project option is defined', () => {
const program = createProgram();
const projectOpt = program.options.find((o) => o.long === '--project');
expect(projectOpt).toBeDefined();
});
it('displays version', () => {