prjct-cli 0.20.0 → 0.20.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/CHANGELOG.md +24 -6
- package/CLAUDE.md +56 -15
- package/README.md +5 -6
- package/bin/prjct +59 -42
- package/bin/prjct.ts +60 -0
- package/core/__tests__/agentic/memory-system.test.ts +18 -3
- package/core/__tests__/agentic/plan-mode.test.ts +55 -26
- package/core/__tests__/agentic/prompt-builder.test.ts +6 -6
- package/core/__tests__/utils/project-commands.test.ts +72 -0
- package/core/agentic/agent-router.ts +3 -12
- package/core/agentic/command-executor.ts +372 -3
- package/core/agentic/context-builder.ts +7 -27
- package/core/agentic/ground-truth.ts +604 -5
- package/core/agentic/index.ts +180 -0
- package/core/agentic/loop-detector.ts +418 -4
- package/core/agentic/memory-system.ts +857 -3
- package/core/agentic/plan-mode.ts +491 -4
- package/core/agentic/prompt-builder.ts +44 -65
- package/core/agentic/services.ts +13 -5
- package/core/agentic/skill-loader.ts +112 -0
- package/core/agentic/smart-context.ts +37 -122
- package/core/agentic/template-loader.ts +79 -122
- package/core/agentic/tool-registry.ts +5 -11
- package/core/agents/index.ts +1 -1
- package/core/agents/performance.ts +4 -2
- package/core/bus/bus.ts +262 -0
- package/core/bus/index.ts +3 -313
- package/core/commands/analysis.ts +5 -5
- package/core/commands/analytics.ts +11 -11
- package/core/commands/base.ts +33 -209
- package/core/commands/cleanup.ts +148 -0
- package/core/commands/command-data.ts +346 -0
- package/core/commands/commands.ts +216 -0
- package/core/commands/design.ts +83 -0
- package/core/commands/index.ts +13 -207
- package/core/commands/maintenance.ts +52 -473
- package/core/commands/planning.ts +3 -3
- package/core/commands/register.ts +104 -0
- package/core/commands/registry.ts +441 -0
- package/core/commands/setup.ts +25 -9
- package/core/commands/shipping.ts +48 -11
- package/core/commands/snapshots.ts +299 -0
- package/core/commands/workflow.ts +2 -2
- package/core/constants/index.ts +254 -4
- package/core/domain/agent-loader.ts +5 -6
- package/core/domain/task-stack.ts +555 -4
- package/core/errors.ts +127 -1
- package/core/events/events.ts +87 -0
- package/core/events/index.ts +4 -138
- package/core/index.ts +15 -23
- package/core/infrastructure/agent-detector.ts +126 -201
- package/core/infrastructure/author-detector.ts +99 -171
- package/core/infrastructure/command-installer.ts +476 -4
- package/core/infrastructure/config-manager.ts +41 -37
- package/core/infrastructure/path-manager.ts +59 -9
- package/core/infrastructure/permission-manager.ts +286 -0
- package/core/outcomes/analyzer.ts +7 -41
- package/core/outcomes/index.ts +1 -1
- package/core/outcomes/recorder.ts +1 -1
- package/core/{plugins → plugin/builtin}/webhook.ts +6 -22
- package/core/plugin/loader.ts +5 -5
- package/core/plugin/registry.ts +2 -2
- package/core/schemas/ideas.ts +85 -54
- package/core/schemas/index.ts +14 -33
- package/core/schemas/permissions.ts +177 -0
- package/core/schemas/project.ts +39 -12
- package/core/schemas/roadmap.ts +94 -59
- package/core/schemas/schemas.ts +39 -0
- package/core/schemas/shipped.ts +87 -60
- package/core/schemas/state.ts +110 -70
- package/core/server/index.ts +21 -0
- package/core/server/routes.ts +165 -0
- package/core/server/server.ts +136 -0
- package/core/server/sse.ts +135 -0
- package/core/services/agent-service.ts +170 -0
- package/core/services/breakdown-service.ts +126 -0
- package/core/services/index.ts +21 -0
- package/core/services/memory-service.ts +108 -0
- package/core/services/project-service.ts +146 -0
- package/core/services/skill-service.ts +253 -0
- package/core/session/compaction.ts +257 -0
- package/core/session/index.ts +20 -8
- package/core/{infrastructure/session-manager/migration.ts → session/log-migration.ts} +9 -9
- package/core/{infrastructure/session-manager/session-manager.ts → session/session-log-manager.ts} +27 -26
- package/core/session/{session-manager.ts → task-session-manager.ts} +7 -4
- package/core/session/utils.ts +1 -1
- package/core/storage/ideas-storage.ts +10 -26
- package/core/storage/index.ts +14 -162
- package/core/storage/queue-storage.ts +13 -11
- package/core/storage/shipped-storage.ts +4 -17
- package/core/storage/state-storage.ts +35 -43
- package/core/storage/storage-manager.ts +42 -52
- package/core/storage/storage.ts +160 -0
- package/core/sync/auth-config.ts +1 -8
- package/core/sync/index.ts +17 -10
- package/core/sync/oauth-handler.ts +1 -6
- package/core/sync/sync-client.ts +6 -34
- package/core/sync/sync-manager.ts +11 -40
- package/core/types/agentic.ts +577 -0
- package/core/types/agents.ts +145 -0
- package/core/types/bus.ts +82 -0
- package/core/types/commands.ts +366 -0
- package/core/types/config.ts +66 -0
- package/core/types/core.ts +96 -0
- package/core/types/domain.ts +71 -0
- package/core/types/events.ts +42 -0
- package/core/types/fs.ts +56 -0
- package/core/types/index.ts +387 -500
- package/core/types/infrastructure.ts +196 -0
- package/core/{agentic/memory-system/types.ts → types/memory.ts} +33 -8
- package/core/{outcomes/types.ts → types/outcomes.ts} +53 -8
- package/core/types/plugin.ts +25 -0
- package/core/types/server.ts +54 -0
- package/core/types/services.ts +65 -0
- package/core/types/session.ts +135 -0
- package/core/types/storage.ts +148 -0
- package/core/types/sync.ts +121 -0
- package/core/types/task.ts +72 -0
- package/core/types/template.ts +24 -0
- package/core/types/utils.ts +90 -0
- package/core/utils/cache.ts +195 -0
- package/core/utils/collection-filters.ts +245 -0
- package/core/utils/date-helper.ts +1 -5
- package/core/utils/file-helper.ts +20 -10
- package/core/utils/jsonl-helper.ts +5 -8
- package/core/utils/markdown-builder.ts +277 -0
- package/core/utils/project-commands.ts +132 -0
- package/core/utils/runtime.ts +119 -0
- package/dist/bin/prjct.mjs +12568 -0
- package/package.json +13 -8
- package/scripts/build.js +106 -0
- package/scripts/postinstall.js +50 -8
- package/templates/agentic/subagent-generation.md +1 -1
- package/templates/commands/serve.md +118 -0
- package/templates/commands/ship.md +13 -2
- package/templates/commands/skill.md +110 -0
- package/templates/commands/sync.md +1 -1
- package/templates/commands/test.md +23 -4
- package/templates/permissions/default.jsonc +60 -0
- package/templates/permissions/permissive.jsonc +49 -0
- package/templates/permissions/strict.jsonc +62 -0
- package/templates/skills/code-review.md +47 -0
- package/templates/skills/debug.md +61 -0
- package/templates/skills/refactor.md +47 -0
- package/templates/subagents/domain/devops.md +1 -1
- package/templates/subagents/domain/testing.md +6 -10
- package/templates/subagents/workflow/prjct-shipper.md +16 -7
- package/templates/tools/bash.txt +22 -0
- package/templates/tools/edit.txt +18 -0
- package/templates/tools/glob.txt +19 -0
- package/templates/tools/grep.txt +21 -0
- package/templates/tools/read.txt +14 -0
- package/templates/tools/task.txt +20 -0
- package/templates/tools/webfetch.txt +16 -0
- package/templates/tools/websearch.txt +18 -0
- package/templates/tools/write.txt +17 -0
- package/core/agentic/command-executor/command-executor.ts +0 -312
- package/core/agentic/command-executor/index.ts +0 -16
- package/core/agentic/command-executor/status-signal.ts +0 -38
- package/core/agentic/command-executor/types.ts +0 -79
- package/core/agentic/ground-truth/index.ts +0 -76
- package/core/agentic/ground-truth/types.ts +0 -33
- package/core/agentic/ground-truth/utils.ts +0 -48
- package/core/agentic/ground-truth/verifiers/analyze.ts +0 -54
- package/core/agentic/ground-truth/verifiers/done.ts +0 -75
- package/core/agentic/ground-truth/verifiers/feature.ts +0 -70
- package/core/agentic/ground-truth/verifiers/index.ts +0 -37
- package/core/agentic/ground-truth/verifiers/init.ts +0 -52
- package/core/agentic/ground-truth/verifiers/now.ts +0 -57
- package/core/agentic/ground-truth/verifiers/ship.ts +0 -85
- package/core/agentic/ground-truth/verifiers/spec.ts +0 -45
- package/core/agentic/ground-truth/verifiers/sync.ts +0 -47
- package/core/agentic/ground-truth/verifiers.ts +0 -6
- package/core/agentic/loop-detector/error-analysis.ts +0 -97
- package/core/agentic/loop-detector/hallucination.ts +0 -71
- package/core/agentic/loop-detector/index.ts +0 -41
- package/core/agentic/loop-detector/loop-detector.ts +0 -222
- package/core/agentic/loop-detector/types.ts +0 -66
- package/core/agentic/memory-system/history.ts +0 -53
- package/core/agentic/memory-system/index.ts +0 -192
- package/core/agentic/memory-system/patterns.ts +0 -156
- package/core/agentic/memory-system/semantic-memories.ts +0 -278
- package/core/agentic/memory-system/session.ts +0 -21
- package/core/agentic/plan-mode/approval.ts +0 -57
- package/core/agentic/plan-mode/constants.ts +0 -44
- package/core/agentic/plan-mode/index.ts +0 -28
- package/core/agentic/plan-mode/plan-mode.ts +0 -407
- package/core/agentic/plan-mode/types.ts +0 -193
- package/core/agents/types.ts +0 -126
- package/core/command-registry/categories.ts +0 -23
- package/core/command-registry/commands.ts +0 -15
- package/core/command-registry/core-commands.ts +0 -344
- package/core/command-registry/index.ts +0 -158
- package/core/command-registry/optional-commands.ts +0 -163
- package/core/command-registry/setup-commands.ts +0 -83
- package/core/command-registry/types.ts +0 -59
- package/core/command-registry.ts +0 -9
- package/core/commands/types.ts +0 -185
- package/core/commands.ts +0 -11
- package/core/constants/formats.ts +0 -187
- package/core/context-sync.ts +0 -18
- package/core/data/index.ts +0 -27
- package/core/data/md-base-manager.ts +0 -203
- package/core/data/md-ideas-manager.ts +0 -155
- package/core/data/md-queue-manager.ts +0 -180
- package/core/data/md-shipped-manager.ts +0 -90
- package/core/data/md-state-manager.ts +0 -137
- package/core/domain/task-stack/index.ts +0 -19
- package/core/domain/task-stack/parser.ts +0 -86
- package/core/domain/task-stack/storage.ts +0 -123
- package/core/domain/task-stack/task-stack.ts +0 -340
- package/core/domain/task-stack/types.ts +0 -51
- package/core/infrastructure/command-installer/command-installer.ts +0 -327
- package/core/infrastructure/command-installer/global-config.ts +0 -136
- package/core/infrastructure/command-installer/index.ts +0 -25
- package/core/infrastructure/command-installer/types.ts +0 -41
- package/core/infrastructure/session-manager/index.ts +0 -23
- package/core/infrastructure/session-manager/types.ts +0 -45
- package/core/infrastructure/session-manager.ts +0 -8
- package/core/serializers/ideas-serializer.ts +0 -187
- package/core/serializers/index.ts +0 -36
- package/core/serializers/queue-serializer.ts +0 -210
- package/core/serializers/shipped-serializer.ts +0 -108
- package/core/serializers/state-serializer.ts +0 -136
- package/core/session/types.ts +0 -29
- /package/core/infrastructure/{agents/claude-agent.ts → claude-agent.ts} +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectService - Project detection, validation, and path resolution
|
|
3
|
+
*
|
|
4
|
+
* Handles project initialization detection, author management, and directory analysis.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import configManager from '../infrastructure/config-manager'
|
|
8
|
+
import pathManager from '../infrastructure/path-manager'
|
|
9
|
+
import authorDetector from '../infrastructure/author-detector'
|
|
10
|
+
import * as fileHelper from '../utils/file-helper'
|
|
11
|
+
import out from '../utils/output'
|
|
12
|
+
import type { Author, CommandResult } from '../types'
|
|
13
|
+
import { ProjectError } from '../errors'
|
|
14
|
+
|
|
15
|
+
export class ProjectService {
|
|
16
|
+
private currentAuthor: Author | null = null
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ensure project is initialized
|
|
20
|
+
*/
|
|
21
|
+
async ensureInit(projectPath: string): Promise<CommandResult> {
|
|
22
|
+
if (await configManager.isConfigured(projectPath)) {
|
|
23
|
+
return { success: true }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
out.spin('initializing project...')
|
|
27
|
+
// Lazy import to avoid circular dependency
|
|
28
|
+
const { PlanningCommands } = await import('../commands/planning')
|
|
29
|
+
const planning = new PlanningCommands()
|
|
30
|
+
const initResult = await planning.init(null, projectPath)
|
|
31
|
+
|
|
32
|
+
if (!initResult.success) {
|
|
33
|
+
return initResult
|
|
34
|
+
}
|
|
35
|
+
return { success: true }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get project ID for a path
|
|
40
|
+
*/
|
|
41
|
+
async getProjectId(projectPath: string): Promise<string> {
|
|
42
|
+
const projectId = await configManager.getProjectId(projectPath)
|
|
43
|
+
if (!projectId) {
|
|
44
|
+
throw ProjectError.notInitialized()
|
|
45
|
+
}
|
|
46
|
+
return projectId
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get global storage path for a project
|
|
51
|
+
*/
|
|
52
|
+
async getGlobalPath(projectPath: string): Promise<string> {
|
|
53
|
+
const projectId = await this.getProjectId(projectPath)
|
|
54
|
+
await pathManager.ensureProjectStructure(projectId)
|
|
55
|
+
return pathManager.getGlobalProjectPath(projectId)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Ensure author information is loaded
|
|
60
|
+
*/
|
|
61
|
+
async ensureAuthor(): Promise<Author> {
|
|
62
|
+
if (this.currentAuthor) return this.currentAuthor
|
|
63
|
+
|
|
64
|
+
const authorObj = await authorDetector.detect()
|
|
65
|
+
this.currentAuthor = {
|
|
66
|
+
name: authorObj.name ?? undefined,
|
|
67
|
+
email: authorObj.email ?? undefined,
|
|
68
|
+
github: authorObj.github ?? undefined,
|
|
69
|
+
}
|
|
70
|
+
return this.currentAuthor
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get current author
|
|
75
|
+
*/
|
|
76
|
+
getCurrentAuthor(): Author | null {
|
|
77
|
+
return this.currentAuthor
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clear cached author (useful for tests)
|
|
82
|
+
*/
|
|
83
|
+
clearAuthorCache(): void {
|
|
84
|
+
this.currentAuthor = null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if directory is empty (excluding common files)
|
|
89
|
+
*/
|
|
90
|
+
async isEmptyDirectory(projectPath: string): Promise<boolean> {
|
|
91
|
+
try {
|
|
92
|
+
const entries = await fileHelper.listFiles(projectPath)
|
|
93
|
+
const meaningfulFiles = entries.filter(
|
|
94
|
+
(name) =>
|
|
95
|
+
!name.startsWith('.') &&
|
|
96
|
+
name !== 'node_modules' &&
|
|
97
|
+
name !== 'package.json' &&
|
|
98
|
+
name !== 'package-lock.json' &&
|
|
99
|
+
name !== 'README.md'
|
|
100
|
+
)
|
|
101
|
+
return meaningfulFiles.length === 0
|
|
102
|
+
} catch {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if directory has existing code
|
|
109
|
+
*/
|
|
110
|
+
async hasExistingCode(projectPath: string): Promise<boolean> {
|
|
111
|
+
try {
|
|
112
|
+
const codePatterns = [
|
|
113
|
+
'src',
|
|
114
|
+
'lib',
|
|
115
|
+
'app',
|
|
116
|
+
'components',
|
|
117
|
+
'pages',
|
|
118
|
+
'api',
|
|
119
|
+
'main.go',
|
|
120
|
+
'main.rs',
|
|
121
|
+
'main.py',
|
|
122
|
+
]
|
|
123
|
+
const entries = await fileHelper.listFiles(projectPath)
|
|
124
|
+
return entries.some((name) => codePatterns.includes(name))
|
|
125
|
+
} catch {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if project is configured
|
|
132
|
+
*/
|
|
133
|
+
async isConfigured(projectPath: string): Promise<boolean> {
|
|
134
|
+
return await configManager.isConfigured(projectPath)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if project needs migration
|
|
139
|
+
*/
|
|
140
|
+
async needsMigration(projectPath: string): Promise<boolean> {
|
|
141
|
+
return await configManager.needsMigration(projectPath)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const projectService = new ProjectService()
|
|
146
|
+
export default projectService
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Service
|
|
3
|
+
*
|
|
4
|
+
* Manages discoverable skills from SKILL.md files.
|
|
5
|
+
* Skills are reusable prompts/instructions with metadata.
|
|
6
|
+
*
|
|
7
|
+
* Skill sources (in priority order):
|
|
8
|
+
* 1. Project: .prjct/skills/*.md
|
|
9
|
+
* 2. Global: ~/.prjct-cli/skills/*.md
|
|
10
|
+
* 3. Built-in: templates/skills/*.md
|
|
11
|
+
*
|
|
12
|
+
* @version 1.0.0
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs/promises'
|
|
16
|
+
import path from 'path'
|
|
17
|
+
import { glob } from 'glob'
|
|
18
|
+
|
|
19
|
+
import type { SkillMetadata, Skill, SkillSearchResult } from '../types'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse YAML-like frontmatter from markdown
|
|
23
|
+
*/
|
|
24
|
+
function parseFrontmatter(content: string): { metadata: Record<string, unknown>; body: string } {
|
|
25
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/
|
|
26
|
+
const match = content.match(frontmatterRegex)
|
|
27
|
+
|
|
28
|
+
if (!match) {
|
|
29
|
+
return { metadata: {}, body: content }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const [, frontmatter, body] = match
|
|
33
|
+
const metadata: Record<string, unknown> = {}
|
|
34
|
+
|
|
35
|
+
// Simple YAML parsing (key: value)
|
|
36
|
+
for (const line of frontmatter.split('\n')) {
|
|
37
|
+
const colonIndex = line.indexOf(':')
|
|
38
|
+
if (colonIndex > 0) {
|
|
39
|
+
const key = line.slice(0, colonIndex).trim()
|
|
40
|
+
let value: unknown = line.slice(colonIndex + 1).trim()
|
|
41
|
+
|
|
42
|
+
// Handle arrays [item1, item2]
|
|
43
|
+
if (typeof value === 'string' && value.startsWith('[') && value.endsWith(']')) {
|
|
44
|
+
value = value.slice(1, -1).split(',').map(s => s.trim().replace(/['"]/g, ''))
|
|
45
|
+
}
|
|
46
|
+
// Remove quotes
|
|
47
|
+
else if (typeof value === 'string' && (value.startsWith('"') || value.startsWith("'"))) {
|
|
48
|
+
value = value.slice(1, -1)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
metadata[key] = value
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { metadata, body: body.trim() }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert filename to skill ID
|
|
60
|
+
*/
|
|
61
|
+
function fileToSkillId(filePath: string): string {
|
|
62
|
+
const basename = path.basename(filePath, '.md')
|
|
63
|
+
return basename.toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class SkillService {
|
|
67
|
+
private skills: Map<string, Skill> = new Map()
|
|
68
|
+
private loaded = false
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get all skill directories in order of priority
|
|
72
|
+
*/
|
|
73
|
+
private getSkillDirs(projectPath?: string): Array<{ dir: string; source: Skill['source'] }> {
|
|
74
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '~'
|
|
75
|
+
const dirs: Array<{ dir: string; source: Skill['source'] }> = []
|
|
76
|
+
|
|
77
|
+
// Project skills (highest priority)
|
|
78
|
+
if (projectPath) {
|
|
79
|
+
dirs.push({ dir: path.join(projectPath, '.prjct', 'skills'), source: 'project' })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Global skills
|
|
83
|
+
dirs.push({ dir: path.join(homeDir, '.prjct-cli', 'skills'), source: 'global' })
|
|
84
|
+
|
|
85
|
+
// Built-in skills (lowest priority)
|
|
86
|
+
dirs.push({ dir: path.join(__dirname, '..', '..', 'templates', 'skills'), source: 'builtin' })
|
|
87
|
+
|
|
88
|
+
return dirs
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load a single skill from file
|
|
93
|
+
*/
|
|
94
|
+
private async loadSkill(filePath: string, source: Skill['source']): Promise<Skill | null> {
|
|
95
|
+
try {
|
|
96
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
97
|
+
const { metadata, body } = parseFrontmatter(content)
|
|
98
|
+
|
|
99
|
+
const id = fileToSkillId(filePath)
|
|
100
|
+
const name = (metadata.name as string) || id
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
id,
|
|
104
|
+
name,
|
|
105
|
+
description: (metadata.description as string) || '',
|
|
106
|
+
content: body,
|
|
107
|
+
source,
|
|
108
|
+
filePath,
|
|
109
|
+
metadata: {
|
|
110
|
+
name,
|
|
111
|
+
description: metadata.description as string,
|
|
112
|
+
agent: metadata.agent as string,
|
|
113
|
+
tags: metadata.tags as string[],
|
|
114
|
+
version: metadata.version as string,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Load all skills from all sources
|
|
124
|
+
*/
|
|
125
|
+
async loadSkills(projectPath?: string): Promise<void> {
|
|
126
|
+
this.skills.clear()
|
|
127
|
+
const dirs = this.getSkillDirs(projectPath)
|
|
128
|
+
|
|
129
|
+
for (const { dir, source } of dirs) {
|
|
130
|
+
try {
|
|
131
|
+
const files = await glob('*.md', { cwd: dir, absolute: true })
|
|
132
|
+
|
|
133
|
+
for (const file of files) {
|
|
134
|
+
const skill = await this.loadSkill(file, source)
|
|
135
|
+
if (skill && !this.skills.has(skill.id)) {
|
|
136
|
+
// Don't override higher priority skills
|
|
137
|
+
this.skills.set(skill.id, skill)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// Directory doesn't exist, skip
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.loaded = true
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get all loaded skills
|
|
150
|
+
*/
|
|
151
|
+
async getAll(projectPath?: string): Promise<Skill[]> {
|
|
152
|
+
if (!this.loaded) {
|
|
153
|
+
await this.loadSkills(projectPath)
|
|
154
|
+
}
|
|
155
|
+
return Array.from(this.skills.values())
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get a skill by ID
|
|
160
|
+
*/
|
|
161
|
+
async get(id: string, projectPath?: string): Promise<Skill | null> {
|
|
162
|
+
if (!this.loaded) {
|
|
163
|
+
await this.loadSkills(projectPath)
|
|
164
|
+
}
|
|
165
|
+
return this.skills.get(id) || null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Search skills by query
|
|
170
|
+
*/
|
|
171
|
+
async search(query: string, projectPath?: string): Promise<SkillSearchResult[]> {
|
|
172
|
+
const skills = await this.getAll(projectPath)
|
|
173
|
+
const queryLower = query.toLowerCase()
|
|
174
|
+
|
|
175
|
+
const results: SkillSearchResult[] = []
|
|
176
|
+
|
|
177
|
+
for (const skill of skills) {
|
|
178
|
+
let relevance = 0
|
|
179
|
+
|
|
180
|
+
// Name match (highest weight)
|
|
181
|
+
if (skill.name.toLowerCase().includes(queryLower)) {
|
|
182
|
+
relevance += 10
|
|
183
|
+
}
|
|
184
|
+
if (skill.id.includes(queryLower)) {
|
|
185
|
+
relevance += 8
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Description match
|
|
189
|
+
if (skill.description.toLowerCase().includes(queryLower)) {
|
|
190
|
+
relevance += 5
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Tag match
|
|
194
|
+
if (skill.metadata.tags?.some(t => t.toLowerCase().includes(queryLower))) {
|
|
195
|
+
relevance += 3
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Content match (lowest weight)
|
|
199
|
+
if (skill.content.toLowerCase().includes(queryLower)) {
|
|
200
|
+
relevance += 1
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (relevance > 0) {
|
|
204
|
+
results.push({ skill, relevance })
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Sort by relevance descending
|
|
209
|
+
return results.sort((a, b) => b.relevance - a.relevance)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* List skills grouped by source
|
|
214
|
+
*/
|
|
215
|
+
async listBySource(projectPath?: string): Promise<Record<Skill['source'], Skill[]>> {
|
|
216
|
+
const skills = await this.getAll(projectPath)
|
|
217
|
+
|
|
218
|
+
const grouped: Record<Skill['source'], Skill[]> = {
|
|
219
|
+
project: [],
|
|
220
|
+
global: [],
|
|
221
|
+
builtin: [],
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const skill of skills) {
|
|
225
|
+
grouped[skill.source].push(skill)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return grouped
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Force reload skills
|
|
233
|
+
*/
|
|
234
|
+
async reload(projectPath?: string): Promise<void> {
|
|
235
|
+
this.loaded = false
|
|
236
|
+
await this.loadSkills(projectPath)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get skill count
|
|
241
|
+
*/
|
|
242
|
+
async count(projectPath?: string): Promise<number> {
|
|
243
|
+
const skills = await this.getAll(projectPath)
|
|
244
|
+
return skills.length
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Singleton instance
|
|
249
|
+
const skillService = new SkillService()
|
|
250
|
+
export default skillService
|
|
251
|
+
|
|
252
|
+
// Export class for testing
|
|
253
|
+
export { SkillService }
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Compaction
|
|
3
|
+
*
|
|
4
|
+
* Compresses conversation context while preserving semantics.
|
|
5
|
+
* Useful for long sessions to prevent context overflow.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by opencode's context management system.
|
|
8
|
+
*
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs/promises'
|
|
13
|
+
import path from 'path'
|
|
14
|
+
import { getTimestamp } from '../utils/date-helper'
|
|
15
|
+
import type { ConversationTurn, CompactedContext, CompactionConfig } from '../types'
|
|
16
|
+
|
|
17
|
+
export type { ConversationTurn, CompactedContext, CompactionConfig } from '../types'
|
|
18
|
+
|
|
19
|
+
const DEFAULT_CONFIG: Required<CompactionConfig> = {
|
|
20
|
+
maxTurns: 50,
|
|
21
|
+
maxTokens: 100000,
|
|
22
|
+
preserveRecent: 10,
|
|
23
|
+
summaryMaxLength: 2000,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Estimate token count (rough approximation)
|
|
28
|
+
*/
|
|
29
|
+
function estimateTokens(text: string): number {
|
|
30
|
+
// Rough estimate: ~4 chars per token
|
|
31
|
+
return Math.ceil(text.length / 4)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract key information from conversation
|
|
36
|
+
*/
|
|
37
|
+
function extractKeyInfo(turns: ConversationTurn[]): {
|
|
38
|
+
decisions: string[]
|
|
39
|
+
filesModified: string[]
|
|
40
|
+
tasksCompleted: string[]
|
|
41
|
+
} {
|
|
42
|
+
const decisions: string[] = []
|
|
43
|
+
const filesModified = new Set<string>()
|
|
44
|
+
const tasksCompleted: string[] = []
|
|
45
|
+
|
|
46
|
+
for (const turn of turns) {
|
|
47
|
+
const content = turn.content
|
|
48
|
+
|
|
49
|
+
// Extract decisions (patterns like "decided to", "will use", "choosing")
|
|
50
|
+
const decisionPatterns = [
|
|
51
|
+
/decided to ([^.]+)/gi,
|
|
52
|
+
/will use ([^.]+)/gi,
|
|
53
|
+
/choosing ([^.]+)/gi,
|
|
54
|
+
/going with ([^.]+)/gi,
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
for (const pattern of decisionPatterns) {
|
|
58
|
+
const matches = content.matchAll(pattern)
|
|
59
|
+
for (const match of matches) {
|
|
60
|
+
decisions.push(match[1].trim())
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Extract file modifications
|
|
65
|
+
const filePatterns = [
|
|
66
|
+
/(?:created|modified|updated|edited|wrote to)\s+[`"]?([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)[`"]?/gi,
|
|
67
|
+
/File (?:created|updated).*?:\s*([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)/gi,
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
for (const pattern of filePatterns) {
|
|
71
|
+
const matches = content.matchAll(pattern)
|
|
72
|
+
for (const match of matches) {
|
|
73
|
+
filesModified.add(match[1])
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Extract completed tasks
|
|
78
|
+
const taskPatterns = [
|
|
79
|
+
/✅\s*(.+)/g,
|
|
80
|
+
/completed[:\s]+(.+)/gi,
|
|
81
|
+
/finished[:\s]+(.+)/gi,
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
for (const pattern of taskPatterns) {
|
|
85
|
+
const matches = content.matchAll(pattern)
|
|
86
|
+
for (const match of matches) {
|
|
87
|
+
const task = match[1].trim()
|
|
88
|
+
if (task.length < 100) { // Avoid capturing large blocks
|
|
89
|
+
tasksCompleted.push(task)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
decisions: [...new Set(decisions)].slice(0, 10),
|
|
97
|
+
filesModified: [...filesModified].slice(0, 20),
|
|
98
|
+
tasksCompleted: [...new Set(tasksCompleted)].slice(0, 10),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate summary from conversation turns
|
|
104
|
+
*/
|
|
105
|
+
function generateSummary(turns: ConversationTurn[], maxLength: number): string {
|
|
106
|
+
// Get key user requests
|
|
107
|
+
const userRequests = turns
|
|
108
|
+
.filter(t => t.role === 'user')
|
|
109
|
+
.map(t => t.content.slice(0, 200))
|
|
110
|
+
.slice(0, 5)
|
|
111
|
+
|
|
112
|
+
// Get key assistant actions
|
|
113
|
+
const assistantActions = turns
|
|
114
|
+
.filter(t => t.role === 'assistant')
|
|
115
|
+
.map(t => {
|
|
116
|
+
// Extract first meaningful sentence
|
|
117
|
+
const firstLine = t.content.split('\n')[0]
|
|
118
|
+
return firstLine.slice(0, 150)
|
|
119
|
+
})
|
|
120
|
+
.filter(a => a.length > 10)
|
|
121
|
+
.slice(0, 5)
|
|
122
|
+
|
|
123
|
+
const summary = [
|
|
124
|
+
'## Session Summary',
|
|
125
|
+
'',
|
|
126
|
+
'### User Requests:',
|
|
127
|
+
...userRequests.map((r, i) => `${i + 1}. ${r.slice(0, 100)}...`),
|
|
128
|
+
'',
|
|
129
|
+
'### Key Actions:',
|
|
130
|
+
...assistantActions.map((a, i) => `${i + 1}. ${a}`),
|
|
131
|
+
].join('\n')
|
|
132
|
+
|
|
133
|
+
return summary.slice(0, maxLength)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Compact conversation context
|
|
138
|
+
*/
|
|
139
|
+
export function compactContext(
|
|
140
|
+
turns: ConversationTurn[],
|
|
141
|
+
config: CompactionConfig = {}
|
|
142
|
+
): CompactedContext {
|
|
143
|
+
const cfg = { ...DEFAULT_CONFIG, ...config }
|
|
144
|
+
|
|
145
|
+
const { decisions, filesModified, tasksCompleted } = extractKeyInfo(turns)
|
|
146
|
+
const summary = generateSummary(turns, cfg.summaryMaxLength)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
summary,
|
|
150
|
+
keyPoints: decisions.slice(0, 5),
|
|
151
|
+
decisions,
|
|
152
|
+
filesModified,
|
|
153
|
+
tasksCompleted,
|
|
154
|
+
originalTurns: turns.length,
|
|
155
|
+
compactedAt: getTimestamp(),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if compaction is needed
|
|
161
|
+
*/
|
|
162
|
+
export function needsCompaction(
|
|
163
|
+
turns: ConversationTurn[],
|
|
164
|
+
config: CompactionConfig = {}
|
|
165
|
+
): boolean {
|
|
166
|
+
const cfg = { ...DEFAULT_CONFIG, ...config }
|
|
167
|
+
|
|
168
|
+
// Check turn count
|
|
169
|
+
if (turns.length > cfg.maxTurns) {
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check token count
|
|
174
|
+
const totalTokens = turns.reduce((sum, t) => sum + estimateTokens(t.content), 0)
|
|
175
|
+
if (totalTokens > cfg.maxTokens) {
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Save compacted context to file
|
|
184
|
+
*/
|
|
185
|
+
export async function saveCompactedContext(
|
|
186
|
+
projectId: string,
|
|
187
|
+
context: CompactedContext
|
|
188
|
+
): Promise<string> {
|
|
189
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '~'
|
|
190
|
+
const dirPath = path.join(homeDir, '.prjct-cli', 'projects', projectId, 'memory')
|
|
191
|
+
const filePath = path.join(dirPath, 'compacted.jsonl')
|
|
192
|
+
|
|
193
|
+
await fs.mkdir(dirPath, { recursive: true })
|
|
194
|
+
|
|
195
|
+
const line = JSON.stringify(context) + '\n'
|
|
196
|
+
await fs.appendFile(filePath, line, 'utf-8')
|
|
197
|
+
|
|
198
|
+
return filePath
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Load recent compacted contexts
|
|
203
|
+
*/
|
|
204
|
+
export async function loadCompactedContexts(
|
|
205
|
+
projectId: string,
|
|
206
|
+
limit = 5
|
|
207
|
+
): Promise<CompactedContext[]> {
|
|
208
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '~'
|
|
209
|
+
const filePath = path.join(
|
|
210
|
+
homeDir, '.prjct-cli', 'projects', projectId, 'memory', 'compacted.jsonl'
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
215
|
+
const lines = content.trim().split('\n').filter(Boolean)
|
|
216
|
+
const contexts = lines.map(line => JSON.parse(line) as CompactedContext)
|
|
217
|
+
|
|
218
|
+
// Return most recent
|
|
219
|
+
return contexts.slice(-limit)
|
|
220
|
+
} catch {
|
|
221
|
+
return []
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Format compacted context for prompt injection
|
|
227
|
+
*/
|
|
228
|
+
export function formatCompactedForPrompt(context: CompactedContext): string {
|
|
229
|
+
const lines = [
|
|
230
|
+
'<compacted-context>',
|
|
231
|
+
context.summary,
|
|
232
|
+
'',
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
if (context.filesModified.length > 0) {
|
|
236
|
+
lines.push('### Files Modified:')
|
|
237
|
+
lines.push(context.filesModified.map(f => `- ${f}`).join('\n'))
|
|
238
|
+
lines.push('')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (context.tasksCompleted.length > 0) {
|
|
242
|
+
lines.push('### Tasks Completed:')
|
|
243
|
+
lines.push(context.tasksCompleted.map(t => `- ${t}`).join('\n'))
|
|
244
|
+
lines.push('')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (context.decisions.length > 0) {
|
|
248
|
+
lines.push('### Decisions Made:')
|
|
249
|
+
lines.push(context.decisions.map(d => `- ${d}`).join('\n'))
|
|
250
|
+
lines.push('')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
lines.push(`*Compacted from ${context.originalTurns} turns at ${context.compactedAt}*`)
|
|
254
|
+
lines.push('</compacted-context>')
|
|
255
|
+
|
|
256
|
+
return lines.join('\n')
|
|
257
|
+
}
|
package/core/session/index.ts
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Session Management - Consolidated Module
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Two managers with distinct purposes:
|
|
5
|
+
* - TaskSessionManager: Task lifecycle (start, pause, complete)
|
|
6
|
+
* - SessionLogManager: Log fragmentation (JSONL temporal)
|
|
6
7
|
*
|
|
7
8
|
* Storage: ~/.prjct-cli/projects/{projectId}/sessions/
|
|
8
9
|
*
|
|
9
|
-
* @version
|
|
10
|
+
* @version 2.0.0
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// Task session types and utilities
|
|
14
|
+
export type { Session, SessionMetrics, TimelineEvent } from '../types'
|
|
13
15
|
export { generateId, calculateDuration, formatDuration } from './utils'
|
|
14
|
-
export { SessionManager } from './session-manager'
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
export
|
|
17
|
+
// Log session types
|
|
18
|
+
export type { SessionEntry, SessionLogMetadata, SessionStats, MigrationResult } from '../types'
|
|
19
|
+
|
|
20
|
+
// Main exports
|
|
21
|
+
export { TaskSessionManager } from './task-session-manager'
|
|
22
|
+
export { SessionLogManager } from './session-log-manager'
|
|
23
|
+
|
|
24
|
+
// Default: TaskSessionManager for backward compatibility
|
|
25
|
+
import { TaskSessionManager } from './task-session-manager'
|
|
26
|
+
export default TaskSessionManager
|
|
27
|
+
|
|
28
|
+
// Alias for backward compatibility
|
|
29
|
+
export { TaskSessionManager as SessionManager } from './task-session-manager'
|