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.
Files changed (226) hide show
  1. package/CHANGELOG.md +24 -6
  2. package/CLAUDE.md +56 -15
  3. package/README.md +5 -6
  4. package/bin/prjct +59 -42
  5. package/bin/prjct.ts +60 -0
  6. package/core/__tests__/agentic/memory-system.test.ts +18 -3
  7. package/core/__tests__/agentic/plan-mode.test.ts +55 -26
  8. package/core/__tests__/agentic/prompt-builder.test.ts +6 -6
  9. package/core/__tests__/utils/project-commands.test.ts +72 -0
  10. package/core/agentic/agent-router.ts +3 -12
  11. package/core/agentic/command-executor.ts +372 -3
  12. package/core/agentic/context-builder.ts +7 -27
  13. package/core/agentic/ground-truth.ts +604 -5
  14. package/core/agentic/index.ts +180 -0
  15. package/core/agentic/loop-detector.ts +418 -4
  16. package/core/agentic/memory-system.ts +857 -3
  17. package/core/agentic/plan-mode.ts +491 -4
  18. package/core/agentic/prompt-builder.ts +44 -65
  19. package/core/agentic/services.ts +13 -5
  20. package/core/agentic/skill-loader.ts +112 -0
  21. package/core/agentic/smart-context.ts +37 -122
  22. package/core/agentic/template-loader.ts +79 -122
  23. package/core/agentic/tool-registry.ts +5 -11
  24. package/core/agents/index.ts +1 -1
  25. package/core/agents/performance.ts +4 -2
  26. package/core/bus/bus.ts +262 -0
  27. package/core/bus/index.ts +3 -313
  28. package/core/commands/analysis.ts +5 -5
  29. package/core/commands/analytics.ts +11 -11
  30. package/core/commands/base.ts +33 -209
  31. package/core/commands/cleanup.ts +148 -0
  32. package/core/commands/command-data.ts +346 -0
  33. package/core/commands/commands.ts +216 -0
  34. package/core/commands/design.ts +83 -0
  35. package/core/commands/index.ts +13 -207
  36. package/core/commands/maintenance.ts +52 -473
  37. package/core/commands/planning.ts +3 -3
  38. package/core/commands/register.ts +104 -0
  39. package/core/commands/registry.ts +441 -0
  40. package/core/commands/setup.ts +25 -9
  41. package/core/commands/shipping.ts +48 -11
  42. package/core/commands/snapshots.ts +299 -0
  43. package/core/commands/workflow.ts +2 -2
  44. package/core/constants/index.ts +254 -4
  45. package/core/domain/agent-loader.ts +5 -6
  46. package/core/domain/task-stack.ts +555 -4
  47. package/core/errors.ts +127 -1
  48. package/core/events/events.ts +87 -0
  49. package/core/events/index.ts +4 -138
  50. package/core/index.ts +15 -23
  51. package/core/infrastructure/agent-detector.ts +126 -201
  52. package/core/infrastructure/author-detector.ts +99 -171
  53. package/core/infrastructure/command-installer.ts +476 -4
  54. package/core/infrastructure/config-manager.ts +41 -37
  55. package/core/infrastructure/path-manager.ts +59 -9
  56. package/core/infrastructure/permission-manager.ts +286 -0
  57. package/core/outcomes/analyzer.ts +7 -41
  58. package/core/outcomes/index.ts +1 -1
  59. package/core/outcomes/recorder.ts +1 -1
  60. package/core/{plugins → plugin/builtin}/webhook.ts +6 -22
  61. package/core/plugin/loader.ts +5 -5
  62. package/core/plugin/registry.ts +2 -2
  63. package/core/schemas/ideas.ts +85 -54
  64. package/core/schemas/index.ts +14 -33
  65. package/core/schemas/permissions.ts +177 -0
  66. package/core/schemas/project.ts +39 -12
  67. package/core/schemas/roadmap.ts +94 -59
  68. package/core/schemas/schemas.ts +39 -0
  69. package/core/schemas/shipped.ts +87 -60
  70. package/core/schemas/state.ts +110 -70
  71. package/core/server/index.ts +21 -0
  72. package/core/server/routes.ts +165 -0
  73. package/core/server/server.ts +136 -0
  74. package/core/server/sse.ts +135 -0
  75. package/core/services/agent-service.ts +170 -0
  76. package/core/services/breakdown-service.ts +126 -0
  77. package/core/services/index.ts +21 -0
  78. package/core/services/memory-service.ts +108 -0
  79. package/core/services/project-service.ts +146 -0
  80. package/core/services/skill-service.ts +253 -0
  81. package/core/session/compaction.ts +257 -0
  82. package/core/session/index.ts +20 -8
  83. package/core/{infrastructure/session-manager/migration.ts → session/log-migration.ts} +9 -9
  84. package/core/{infrastructure/session-manager/session-manager.ts → session/session-log-manager.ts} +27 -26
  85. package/core/session/{session-manager.ts → task-session-manager.ts} +7 -4
  86. package/core/session/utils.ts +1 -1
  87. package/core/storage/ideas-storage.ts +10 -26
  88. package/core/storage/index.ts +14 -162
  89. package/core/storage/queue-storage.ts +13 -11
  90. package/core/storage/shipped-storage.ts +4 -17
  91. package/core/storage/state-storage.ts +35 -43
  92. package/core/storage/storage-manager.ts +42 -52
  93. package/core/storage/storage.ts +160 -0
  94. package/core/sync/auth-config.ts +1 -8
  95. package/core/sync/index.ts +17 -10
  96. package/core/sync/oauth-handler.ts +1 -6
  97. package/core/sync/sync-client.ts +6 -34
  98. package/core/sync/sync-manager.ts +11 -40
  99. package/core/types/agentic.ts +577 -0
  100. package/core/types/agents.ts +145 -0
  101. package/core/types/bus.ts +82 -0
  102. package/core/types/commands.ts +366 -0
  103. package/core/types/config.ts +66 -0
  104. package/core/types/core.ts +96 -0
  105. package/core/types/domain.ts +71 -0
  106. package/core/types/events.ts +42 -0
  107. package/core/types/fs.ts +56 -0
  108. package/core/types/index.ts +387 -500
  109. package/core/types/infrastructure.ts +196 -0
  110. package/core/{agentic/memory-system/types.ts → types/memory.ts} +33 -8
  111. package/core/{outcomes/types.ts → types/outcomes.ts} +53 -8
  112. package/core/types/plugin.ts +25 -0
  113. package/core/types/server.ts +54 -0
  114. package/core/types/services.ts +65 -0
  115. package/core/types/session.ts +135 -0
  116. package/core/types/storage.ts +148 -0
  117. package/core/types/sync.ts +121 -0
  118. package/core/types/task.ts +72 -0
  119. package/core/types/template.ts +24 -0
  120. package/core/types/utils.ts +90 -0
  121. package/core/utils/cache.ts +195 -0
  122. package/core/utils/collection-filters.ts +245 -0
  123. package/core/utils/date-helper.ts +1 -5
  124. package/core/utils/file-helper.ts +20 -10
  125. package/core/utils/jsonl-helper.ts +5 -8
  126. package/core/utils/markdown-builder.ts +277 -0
  127. package/core/utils/project-commands.ts +132 -0
  128. package/core/utils/runtime.ts +119 -0
  129. package/dist/bin/prjct.mjs +12568 -0
  130. package/package.json +13 -8
  131. package/scripts/build.js +106 -0
  132. package/scripts/postinstall.js +50 -8
  133. package/templates/agentic/subagent-generation.md +1 -1
  134. package/templates/commands/serve.md +118 -0
  135. package/templates/commands/ship.md +13 -2
  136. package/templates/commands/skill.md +110 -0
  137. package/templates/commands/sync.md +1 -1
  138. package/templates/commands/test.md +23 -4
  139. package/templates/permissions/default.jsonc +60 -0
  140. package/templates/permissions/permissive.jsonc +49 -0
  141. package/templates/permissions/strict.jsonc +62 -0
  142. package/templates/skills/code-review.md +47 -0
  143. package/templates/skills/debug.md +61 -0
  144. package/templates/skills/refactor.md +47 -0
  145. package/templates/subagents/domain/devops.md +1 -1
  146. package/templates/subagents/domain/testing.md +6 -10
  147. package/templates/subagents/workflow/prjct-shipper.md +16 -7
  148. package/templates/tools/bash.txt +22 -0
  149. package/templates/tools/edit.txt +18 -0
  150. package/templates/tools/glob.txt +19 -0
  151. package/templates/tools/grep.txt +21 -0
  152. package/templates/tools/read.txt +14 -0
  153. package/templates/tools/task.txt +20 -0
  154. package/templates/tools/webfetch.txt +16 -0
  155. package/templates/tools/websearch.txt +18 -0
  156. package/templates/tools/write.txt +17 -0
  157. package/core/agentic/command-executor/command-executor.ts +0 -312
  158. package/core/agentic/command-executor/index.ts +0 -16
  159. package/core/agentic/command-executor/status-signal.ts +0 -38
  160. package/core/agentic/command-executor/types.ts +0 -79
  161. package/core/agentic/ground-truth/index.ts +0 -76
  162. package/core/agentic/ground-truth/types.ts +0 -33
  163. package/core/agentic/ground-truth/utils.ts +0 -48
  164. package/core/agentic/ground-truth/verifiers/analyze.ts +0 -54
  165. package/core/agentic/ground-truth/verifiers/done.ts +0 -75
  166. package/core/agentic/ground-truth/verifiers/feature.ts +0 -70
  167. package/core/agentic/ground-truth/verifiers/index.ts +0 -37
  168. package/core/agentic/ground-truth/verifiers/init.ts +0 -52
  169. package/core/agentic/ground-truth/verifiers/now.ts +0 -57
  170. package/core/agentic/ground-truth/verifiers/ship.ts +0 -85
  171. package/core/agentic/ground-truth/verifiers/spec.ts +0 -45
  172. package/core/agentic/ground-truth/verifiers/sync.ts +0 -47
  173. package/core/agentic/ground-truth/verifiers.ts +0 -6
  174. package/core/agentic/loop-detector/error-analysis.ts +0 -97
  175. package/core/agentic/loop-detector/hallucination.ts +0 -71
  176. package/core/agentic/loop-detector/index.ts +0 -41
  177. package/core/agentic/loop-detector/loop-detector.ts +0 -222
  178. package/core/agentic/loop-detector/types.ts +0 -66
  179. package/core/agentic/memory-system/history.ts +0 -53
  180. package/core/agentic/memory-system/index.ts +0 -192
  181. package/core/agentic/memory-system/patterns.ts +0 -156
  182. package/core/agentic/memory-system/semantic-memories.ts +0 -278
  183. package/core/agentic/memory-system/session.ts +0 -21
  184. package/core/agentic/plan-mode/approval.ts +0 -57
  185. package/core/agentic/plan-mode/constants.ts +0 -44
  186. package/core/agentic/plan-mode/index.ts +0 -28
  187. package/core/agentic/plan-mode/plan-mode.ts +0 -407
  188. package/core/agentic/plan-mode/types.ts +0 -193
  189. package/core/agents/types.ts +0 -126
  190. package/core/command-registry/categories.ts +0 -23
  191. package/core/command-registry/commands.ts +0 -15
  192. package/core/command-registry/core-commands.ts +0 -344
  193. package/core/command-registry/index.ts +0 -158
  194. package/core/command-registry/optional-commands.ts +0 -163
  195. package/core/command-registry/setup-commands.ts +0 -83
  196. package/core/command-registry/types.ts +0 -59
  197. package/core/command-registry.ts +0 -9
  198. package/core/commands/types.ts +0 -185
  199. package/core/commands.ts +0 -11
  200. package/core/constants/formats.ts +0 -187
  201. package/core/context-sync.ts +0 -18
  202. package/core/data/index.ts +0 -27
  203. package/core/data/md-base-manager.ts +0 -203
  204. package/core/data/md-ideas-manager.ts +0 -155
  205. package/core/data/md-queue-manager.ts +0 -180
  206. package/core/data/md-shipped-manager.ts +0 -90
  207. package/core/data/md-state-manager.ts +0 -137
  208. package/core/domain/task-stack/index.ts +0 -19
  209. package/core/domain/task-stack/parser.ts +0 -86
  210. package/core/domain/task-stack/storage.ts +0 -123
  211. package/core/domain/task-stack/task-stack.ts +0 -340
  212. package/core/domain/task-stack/types.ts +0 -51
  213. package/core/infrastructure/command-installer/command-installer.ts +0 -327
  214. package/core/infrastructure/command-installer/global-config.ts +0 -136
  215. package/core/infrastructure/command-installer/index.ts +0 -25
  216. package/core/infrastructure/command-installer/types.ts +0 -41
  217. package/core/infrastructure/session-manager/index.ts +0 -23
  218. package/core/infrastructure/session-manager/types.ts +0 -45
  219. package/core/infrastructure/session-manager.ts +0 -8
  220. package/core/serializers/ideas-serializer.ts +0 -187
  221. package/core/serializers/index.ts +0 -36
  222. package/core/serializers/queue-serializer.ts +0 -210
  223. package/core/serializers/shipped-serializer.ts +0 -108
  224. package/core/serializers/state-serializer.ts +0 -136
  225. package/core/session/types.ts +0 -29
  226. /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
+ }
@@ -1,17 +1,29 @@
1
1
  /**
2
- * SessionManager - Structured Session Tracking
2
+ * Session Management - Consolidated Module
3
3
  *
4
- * Tracks work sessions with metrics, timeline, and duration.
5
- * Inspired by OpenCode's session system but simplified.
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 1.0.0
10
+ * @version 2.0.0
10
11
  */
11
12
 
12
- export type { Session, SessionMetrics, TimelineEvent } from './types'
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
- import { SessionManager } from './session-manager'
17
- export default SessionManager
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'