prjct-cli 0.20.0 → 0.21.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.
Files changed (236) 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/integrations/notion/client.ts +323 -0
  58. package/core/integrations/notion/index.ts +43 -0
  59. package/core/integrations/notion/setup.ts +230 -0
  60. package/core/integrations/notion/sync.ts +311 -0
  61. package/core/integrations/notion/templates.ts +234 -0
  62. package/core/outcomes/analyzer.ts +7 -41
  63. package/core/outcomes/index.ts +1 -1
  64. package/core/outcomes/recorder.ts +1 -1
  65. package/core/plugin/builtin/notion.ts +178 -0
  66. package/core/{plugins → plugin/builtin}/webhook.ts +6 -22
  67. package/core/plugin/loader.ts +5 -5
  68. package/core/plugin/registry.ts +2 -2
  69. package/core/schemas/ideas.ts +85 -54
  70. package/core/schemas/index.ts +14 -33
  71. package/core/schemas/permissions.ts +177 -0
  72. package/core/schemas/project.ts +39 -12
  73. package/core/schemas/roadmap.ts +94 -59
  74. package/core/schemas/schemas.ts +39 -0
  75. package/core/schemas/shipped.ts +87 -60
  76. package/core/schemas/state.ts +110 -70
  77. package/core/server/index.ts +21 -0
  78. package/core/server/routes.ts +165 -0
  79. package/core/server/server.ts +136 -0
  80. package/core/server/sse.ts +135 -0
  81. package/core/services/agent-service.ts +170 -0
  82. package/core/services/breakdown-service.ts +126 -0
  83. package/core/services/index.ts +21 -0
  84. package/core/services/memory-service.ts +108 -0
  85. package/core/services/project-service.ts +146 -0
  86. package/core/services/skill-service.ts +253 -0
  87. package/core/session/compaction.ts +257 -0
  88. package/core/session/index.ts +20 -8
  89. package/core/{infrastructure/session-manager/migration.ts → session/log-migration.ts} +9 -9
  90. package/core/{infrastructure/session-manager/session-manager.ts → session/session-log-manager.ts} +27 -26
  91. package/core/session/{session-manager.ts → task-session-manager.ts} +7 -4
  92. package/core/session/utils.ts +1 -1
  93. package/core/storage/ideas-storage.ts +10 -26
  94. package/core/storage/index.ts +14 -162
  95. package/core/storage/queue-storage.ts +13 -11
  96. package/core/storage/shipped-storage.ts +4 -17
  97. package/core/storage/state-storage.ts +35 -43
  98. package/core/storage/storage-manager.ts +42 -52
  99. package/core/storage/storage.ts +160 -0
  100. package/core/sync/auth-config.ts +1 -8
  101. package/core/sync/index.ts +17 -10
  102. package/core/sync/oauth-handler.ts +1 -6
  103. package/core/sync/sync-client.ts +6 -34
  104. package/core/sync/sync-manager.ts +11 -40
  105. package/core/types/agentic.ts +577 -0
  106. package/core/types/agents.ts +145 -0
  107. package/core/types/bus.ts +82 -0
  108. package/core/types/commands.ts +366 -0
  109. package/core/types/config.ts +70 -0
  110. package/core/types/core.ts +96 -0
  111. package/core/types/domain.ts +71 -0
  112. package/core/types/events.ts +42 -0
  113. package/core/types/fs.ts +56 -0
  114. package/core/types/index.ts +396 -500
  115. package/core/types/infrastructure.ts +196 -0
  116. package/core/types/integrations.ts +57 -0
  117. package/core/{agentic/memory-system/types.ts → types/memory.ts} +33 -8
  118. package/core/{outcomes/types.ts → types/outcomes.ts} +53 -8
  119. package/core/types/plugin.ts +25 -0
  120. package/core/types/server.ts +54 -0
  121. package/core/types/services.ts +65 -0
  122. package/core/types/session.ts +135 -0
  123. package/core/types/storage.ts +148 -0
  124. package/core/types/sync.ts +121 -0
  125. package/core/types/task.ts +72 -0
  126. package/core/types/template.ts +24 -0
  127. package/core/types/utils.ts +90 -0
  128. package/core/utils/cache.ts +195 -0
  129. package/core/utils/collection-filters.ts +245 -0
  130. package/core/utils/date-helper.ts +1 -5
  131. package/core/utils/file-helper.ts +20 -10
  132. package/core/utils/jsonl-helper.ts +5 -8
  133. package/core/utils/markdown-builder.ts +277 -0
  134. package/core/utils/project-commands.ts +132 -0
  135. package/core/utils/runtime.ts +119 -0
  136. package/dist/bin/prjct.mjs +12568 -0
  137. package/package.json +13 -8
  138. package/scripts/build.js +106 -0
  139. package/scripts/postinstall.js +50 -8
  140. package/templates/agentic/subagent-generation.md +1 -1
  141. package/templates/commands/init.md +43 -0
  142. package/templates/commands/notion-setup.md +191 -0
  143. package/templates/commands/serve.md +118 -0
  144. package/templates/commands/ship.md +13 -2
  145. package/templates/commands/skill.md +110 -0
  146. package/templates/commands/sync.md +1 -1
  147. package/templates/commands/test.md +23 -4
  148. package/templates/mcp-config.json +28 -0
  149. package/templates/permissions/default.jsonc +60 -0
  150. package/templates/permissions/permissive.jsonc +49 -0
  151. package/templates/permissions/strict.jsonc +62 -0
  152. package/templates/skills/code-review.md +47 -0
  153. package/templates/skills/debug.md +61 -0
  154. package/templates/skills/refactor.md +47 -0
  155. package/templates/subagents/domain/devops.md +1 -1
  156. package/templates/subagents/domain/testing.md +6 -10
  157. package/templates/subagents/workflow/prjct-shipper.md +16 -7
  158. package/templates/tools/bash.txt +22 -0
  159. package/templates/tools/edit.txt +18 -0
  160. package/templates/tools/glob.txt +19 -0
  161. package/templates/tools/grep.txt +21 -0
  162. package/templates/tools/read.txt +14 -0
  163. package/templates/tools/task.txt +20 -0
  164. package/templates/tools/webfetch.txt +16 -0
  165. package/templates/tools/websearch.txt +18 -0
  166. package/templates/tools/write.txt +17 -0
  167. package/core/agentic/command-executor/command-executor.ts +0 -312
  168. package/core/agentic/command-executor/index.ts +0 -16
  169. package/core/agentic/command-executor/status-signal.ts +0 -38
  170. package/core/agentic/command-executor/types.ts +0 -79
  171. package/core/agentic/ground-truth/index.ts +0 -76
  172. package/core/agentic/ground-truth/types.ts +0 -33
  173. package/core/agentic/ground-truth/utils.ts +0 -48
  174. package/core/agentic/ground-truth/verifiers/analyze.ts +0 -54
  175. package/core/agentic/ground-truth/verifiers/done.ts +0 -75
  176. package/core/agentic/ground-truth/verifiers/feature.ts +0 -70
  177. package/core/agentic/ground-truth/verifiers/index.ts +0 -37
  178. package/core/agentic/ground-truth/verifiers/init.ts +0 -52
  179. package/core/agentic/ground-truth/verifiers/now.ts +0 -57
  180. package/core/agentic/ground-truth/verifiers/ship.ts +0 -85
  181. package/core/agentic/ground-truth/verifiers/spec.ts +0 -45
  182. package/core/agentic/ground-truth/verifiers/sync.ts +0 -47
  183. package/core/agentic/ground-truth/verifiers.ts +0 -6
  184. package/core/agentic/loop-detector/error-analysis.ts +0 -97
  185. package/core/agentic/loop-detector/hallucination.ts +0 -71
  186. package/core/agentic/loop-detector/index.ts +0 -41
  187. package/core/agentic/loop-detector/loop-detector.ts +0 -222
  188. package/core/agentic/loop-detector/types.ts +0 -66
  189. package/core/agentic/memory-system/history.ts +0 -53
  190. package/core/agentic/memory-system/index.ts +0 -192
  191. package/core/agentic/memory-system/patterns.ts +0 -156
  192. package/core/agentic/memory-system/semantic-memories.ts +0 -278
  193. package/core/agentic/memory-system/session.ts +0 -21
  194. package/core/agentic/plan-mode/approval.ts +0 -57
  195. package/core/agentic/plan-mode/constants.ts +0 -44
  196. package/core/agentic/plan-mode/index.ts +0 -28
  197. package/core/agentic/plan-mode/plan-mode.ts +0 -407
  198. package/core/agentic/plan-mode/types.ts +0 -193
  199. package/core/agents/types.ts +0 -126
  200. package/core/command-registry/categories.ts +0 -23
  201. package/core/command-registry/commands.ts +0 -15
  202. package/core/command-registry/core-commands.ts +0 -344
  203. package/core/command-registry/index.ts +0 -158
  204. package/core/command-registry/optional-commands.ts +0 -163
  205. package/core/command-registry/setup-commands.ts +0 -83
  206. package/core/command-registry/types.ts +0 -59
  207. package/core/command-registry.ts +0 -9
  208. package/core/commands/types.ts +0 -185
  209. package/core/commands.ts +0 -11
  210. package/core/constants/formats.ts +0 -187
  211. package/core/context-sync.ts +0 -18
  212. package/core/data/index.ts +0 -27
  213. package/core/data/md-base-manager.ts +0 -203
  214. package/core/data/md-ideas-manager.ts +0 -155
  215. package/core/data/md-queue-manager.ts +0 -180
  216. package/core/data/md-shipped-manager.ts +0 -90
  217. package/core/data/md-state-manager.ts +0 -137
  218. package/core/domain/task-stack/index.ts +0 -19
  219. package/core/domain/task-stack/parser.ts +0 -86
  220. package/core/domain/task-stack/storage.ts +0 -123
  221. package/core/domain/task-stack/task-stack.ts +0 -340
  222. package/core/domain/task-stack/types.ts +0 -51
  223. package/core/infrastructure/command-installer/command-installer.ts +0 -327
  224. package/core/infrastructure/command-installer/global-config.ts +0 -136
  225. package/core/infrastructure/command-installer/index.ts +0 -25
  226. package/core/infrastructure/command-installer/types.ts +0 -41
  227. package/core/infrastructure/session-manager/index.ts +0 -23
  228. package/core/infrastructure/session-manager/types.ts +0 -45
  229. package/core/infrastructure/session-manager.ts +0 -8
  230. package/core/serializers/ideas-serializer.ts +0 -187
  231. package/core/serializers/index.ts +0 -36
  232. package/core/serializers/queue-serializer.ts +0 -210
  233. package/core/serializers/shipped-serializer.ts +0 -108
  234. package/core/serializers/state-serializer.ts +0 -136
  235. package/core/session/types.ts +0 -29
  236. /package/core/infrastructure/{agents/claude-agent.ts → claude-agent.ts} +0 -0
@@ -15,14 +15,7 @@ import crypto from 'crypto'
15
15
  import os from 'os'
16
16
  import * as dateHelper from '../utils/date-helper'
17
17
  import * as fileHelper from '../utils/file-helper'
18
-
19
- interface SessionInfo {
20
- year: string
21
- month: string
22
- day: string
23
- path: string
24
- date: Date
25
- }
18
+ import type { SessionInfo } from '../types'
26
19
 
27
20
  class PathManager {
28
21
  globalBaseDir: string
@@ -30,7 +23,21 @@ class PathManager {
30
23
  globalConfigDir: string
31
24
 
32
25
  constructor() {
33
- this.globalBaseDir = path.join(os.homedir(), '.prjct-cli')
26
+ const envOverride = process.env.PRJCT_CLI_HOME?.trim()
27
+ this.globalBaseDir = envOverride ? path.resolve(envOverride) : path.join(os.homedir(), '.prjct-cli')
28
+ this.globalProjectsDir = path.join(this.globalBaseDir, 'projects')
29
+ this.globalConfigDir = path.join(this.globalBaseDir, 'config')
30
+ }
31
+
32
+ /**
33
+ * Override global storage location (primarily for tests and sandboxed environments).
34
+ *
35
+ * When unset, global storage defaults to `~/.prjct-cli/`.
36
+ *
37
+ * @param {string} globalBaseDir - Base directory that will contain `projects/` and `config/`.
38
+ */
39
+ setGlobalBaseDir(globalBaseDir: string): void {
40
+ this.globalBaseDir = path.resolve(globalBaseDir)
34
41
  this.globalProjectsDir = path.join(this.globalBaseDir, 'projects')
35
42
  this.globalConfigDir = path.join(this.globalBaseDir, 'config')
36
43
  }
@@ -274,6 +281,49 @@ class PathManager {
274
281
  getLastSyncPath(projectId: string): string {
275
282
  return path.join(this.getGlobalProjectPath(projectId), 'sync', 'last-sync.json')
276
283
  }
284
+
285
+ /**
286
+ * Get the running status file path (for status signal)
287
+ * Used to indicate when prjct CLI is actively running
288
+ */
289
+ getRunningStatusPath(): string {
290
+ return path.join(this.globalBaseDir, '.running')
291
+ }
292
+
293
+ /**
294
+ * Get the docs directory path
295
+ * Contains documentation and help files
296
+ */
297
+ getDocsPath(): string {
298
+ return path.join(this.globalBaseDir, 'docs')
299
+ }
300
+
301
+ /**
302
+ * Get the agents directory path for a project
303
+ * If projectId is null, returns the global agents directory
304
+ */
305
+ getAgentsPath(projectId: string | null): string {
306
+ if (projectId) {
307
+ return path.join(this.getGlobalProjectPath(projectId), 'agents')
308
+ }
309
+ return path.join(this.globalBaseDir, 'agents')
310
+ }
311
+
312
+ /**
313
+ * Get the storage file path for a project
314
+ * Convenience method for accessing storage layer files
315
+ */
316
+ getStoragePath(projectId: string, filename: string): string {
317
+ return path.join(this.getGlobalProjectPath(projectId), 'storage', filename)
318
+ }
319
+
320
+ /**
321
+ * Get the context directory path for a project
322
+ * Contains generated markdown context files
323
+ */
324
+ getContextPath(projectId: string): string {
325
+ return path.join(this.getGlobalProjectPath(projectId), 'context')
326
+ }
277
327
  }
278
328
 
279
329
  const pathManager = new PathManager()
@@ -0,0 +1,286 @@
1
+ /**
2
+ * PermissionManager - Granular permission control for CLI operations
3
+ *
4
+ * Implements glob-based permission matching inspired by opencode.
5
+ * Checks bash commands, file operations, and web access against
6
+ * configurable permission rules.
7
+ *
8
+ * @version 1.0.0
9
+ */
10
+
11
+ import {
12
+ type PermissionsConfig,
13
+ type PermissionLevel,
14
+ buildDefaultPermissions,
15
+ } from '../schemas/permissions'
16
+ import type { PermissionCheckResult } from '../types'
17
+
18
+ /**
19
+ * Simple glob pattern matching
20
+ * Supports * (any chars) and ? (single char)
21
+ */
22
+ function matchGlobPattern(pattern: string, text: string): boolean {
23
+ // Escape regex special chars except * and ?
24
+ const regexPattern = pattern
25
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
26
+ .replace(/\*/g, '.*')
27
+ .replace(/\?/g, '.')
28
+
29
+ const regex = new RegExp(`^${regexPattern}$`, 'i')
30
+ return regex.test(text)
31
+ }
32
+
33
+ /**
34
+ * Find the most specific matching pattern
35
+ * More specific = longer pattern without wildcards
36
+ */
37
+ function findBestMatch(
38
+ patterns: Record<string, PermissionLevel>,
39
+ text: string
40
+ ): { pattern: string; level: PermissionLevel } | null {
41
+ let bestMatch: { pattern: string; level: PermissionLevel; specificity: number } | null = null
42
+
43
+ for (const [pattern, level] of Object.entries(patterns)) {
44
+ if (matchGlobPattern(pattern, text)) {
45
+ // Calculate specificity: longer patterns without wildcards are more specific
46
+ const wildcardCount = (pattern.match(/\*/g) || []).length
47
+ const specificity = pattern.length - wildcardCount * 10
48
+
49
+ if (!bestMatch || specificity > bestMatch.specificity) {
50
+ bestMatch = { pattern, level, specificity }
51
+ }
52
+ }
53
+ }
54
+
55
+ return bestMatch ? { pattern: bestMatch.pattern, level: bestMatch.level } : null
56
+ }
57
+
58
+ class PermissionManager {
59
+ private config: PermissionsConfig
60
+
61
+ constructor(config?: PermissionsConfig) {
62
+ this.config = config || buildDefaultPermissions()
63
+ }
64
+
65
+ /**
66
+ * Update the permissions configuration
67
+ */
68
+ setConfig(config: PermissionsConfig): void {
69
+ this.config = config
70
+ }
71
+
72
+ /**
73
+ * Merge custom permissions with defaults
74
+ */
75
+ mergeWithDefaults(custom: Partial<PermissionsConfig>): PermissionsConfig {
76
+ const defaults = buildDefaultPermissions()
77
+ return {
78
+ ...defaults,
79
+ ...custom,
80
+ bash: { ...defaults.bash, ...custom.bash },
81
+ files: {
82
+ read: { ...defaults.files?.read, ...custom.files?.read },
83
+ write: { ...defaults.files?.write, ...custom.files?.write },
84
+ delete: { ...defaults.files?.delete, ...custom.files?.delete },
85
+ },
86
+ web: {
87
+ enabled: custom.web?.enabled ?? defaults.web?.enabled ?? true,
88
+ allowedDomains: custom.web?.allowedDomains ?? defaults.web?.allowedDomains,
89
+ blockedDomains: custom.web?.blockedDomains ?? defaults.web?.blockedDomains,
90
+ },
91
+ doomLoop: {
92
+ enabled: custom.doomLoop?.enabled ?? defaults.doomLoop?.enabled ?? true,
93
+ maxRetries: custom.doomLoop?.maxRetries ?? defaults.doomLoop?.maxRetries ?? 3,
94
+ },
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Check if a bash command is allowed
100
+ */
101
+ checkBashCommand(command: string): PermissionCheckResult {
102
+ if (!this.config.bash) {
103
+ return { allowed: true, level: 'allow', reason: 'No bash permissions configured' }
104
+ }
105
+
106
+ const match = findBestMatch(this.config.bash, command)
107
+
108
+ if (!match) {
109
+ // Default: allow if no pattern matches
110
+ return { allowed: true, level: 'allow', reason: 'No matching pattern' }
111
+ }
112
+
113
+ return {
114
+ allowed: match.level === 'allow',
115
+ level: match.level,
116
+ matchedPattern: match.pattern,
117
+ reason: match.level === 'deny'
118
+ ? `Command denied by pattern: ${match.pattern}`
119
+ : match.level === 'ask'
120
+ ? `Command requires approval: ${match.pattern}`
121
+ : undefined,
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Check if a file operation is allowed
127
+ */
128
+ checkFileOperation(
129
+ operation: 'read' | 'write' | 'delete',
130
+ filePath: string
131
+ ): PermissionCheckResult {
132
+ const filePerms = this.config.files?.[operation]
133
+
134
+ if (!filePerms) {
135
+ return { allowed: true, level: 'allow', reason: 'No file permissions configured' }
136
+ }
137
+
138
+ const match = findBestMatch(filePerms, filePath)
139
+
140
+ if (!match) {
141
+ return { allowed: true, level: 'allow', reason: 'No matching pattern' }
142
+ }
143
+
144
+ return {
145
+ allowed: match.level === 'allow',
146
+ level: match.level,
147
+ matchedPattern: match.pattern,
148
+ reason: match.level === 'deny'
149
+ ? `File operation denied: ${operation} on ${match.pattern}`
150
+ : match.level === 'ask'
151
+ ? `File operation requires approval: ${operation}`
152
+ : undefined,
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Check if web fetch is allowed for a domain
158
+ */
159
+ checkWebFetch(url: string): PermissionCheckResult {
160
+ const webConfig = this.config.web
161
+
162
+ if (!webConfig?.enabled) {
163
+ return {
164
+ allowed: false,
165
+ level: 'deny',
166
+ reason: 'Web fetch is disabled',
167
+ }
168
+ }
169
+
170
+ try {
171
+ const domain = new URL(url).hostname
172
+
173
+ // Check blocked domains
174
+ if (webConfig.blockedDomains?.some((d) => domain.includes(d))) {
175
+ return {
176
+ allowed: false,
177
+ level: 'deny',
178
+ matchedPattern: domain,
179
+ reason: `Domain is blocked: ${domain}`,
180
+ }
181
+ }
182
+
183
+ // Check allowed domains (if specified, only those are allowed)
184
+ if (webConfig.allowedDomains && webConfig.allowedDomains.length > 0) {
185
+ const isAllowed = webConfig.allowedDomains.some((d) => domain.includes(d))
186
+ if (!isAllowed) {
187
+ return {
188
+ allowed: false,
189
+ level: 'deny',
190
+ matchedPattern: domain,
191
+ reason: `Domain not in allowed list: ${domain}`,
192
+ }
193
+ }
194
+ }
195
+
196
+ return { allowed: true, level: 'allow' }
197
+ } catch {
198
+ return {
199
+ allowed: false,
200
+ level: 'deny',
201
+ reason: 'Invalid URL',
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Check if a skill can be invoked
208
+ */
209
+ checkSkill(skillName: string): PermissionCheckResult {
210
+ if (!this.config.skills) {
211
+ return { allowed: true, level: 'allow', reason: 'No skill permissions configured' }
212
+ }
213
+
214
+ const match = findBestMatch(this.config.skills, skillName)
215
+
216
+ if (!match) {
217
+ return { allowed: true, level: 'allow', reason: 'No matching pattern' }
218
+ }
219
+
220
+ return {
221
+ allowed: match.level === 'allow',
222
+ level: match.level,
223
+ matchedPattern: match.pattern,
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Check if external directory access is allowed
229
+ */
230
+ checkExternalDirectory(path: string, projectRoot: string): PermissionCheckResult {
231
+ const isExternal = !path.startsWith(projectRoot)
232
+
233
+ if (!isExternal) {
234
+ return { allowed: true, level: 'allow', reason: 'Path is within project' }
235
+ }
236
+
237
+ const level = this.config.externalDirectories || 'ask'
238
+
239
+ return {
240
+ allowed: level === 'allow',
241
+ level,
242
+ reason: level === 'deny'
243
+ ? 'External directory access denied'
244
+ : level === 'ask'
245
+ ? 'External directory access requires approval'
246
+ : undefined,
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Get current permissions config
252
+ */
253
+ getConfig(): PermissionsConfig {
254
+ return this.config
255
+ }
256
+
257
+ /**
258
+ * Check doom loop protection
259
+ */
260
+ checkDoomLoop(retryCount: number): PermissionCheckResult {
261
+ const doomLoop = this.config.doomLoop
262
+
263
+ if (!doomLoop?.enabled) {
264
+ return { allowed: true, level: 'allow', reason: 'Doom loop protection disabled' }
265
+ }
266
+
267
+ const maxRetries = doomLoop.maxRetries || 3
268
+
269
+ if (retryCount >= maxRetries) {
270
+ return {
271
+ allowed: false,
272
+ level: 'deny',
273
+ reason: `Doom loop detected: ${retryCount} retries exceeded limit of ${maxRetries}`,
274
+ }
275
+ }
276
+
277
+ return { allowed: true, level: 'allow' }
278
+ }
279
+ }
280
+
281
+ // Singleton instance
282
+ const permissionManager = new PermissionManager()
283
+ export default permissionManager
284
+
285
+ // Export class for testing
286
+ export { PermissionManager }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Notion Client
3
+ * Wrapper for Notion MCP tools with fallback to direct API.
4
+ *
5
+ * Uses MCP tools when available (via Claude), falls back to fetch for CLI usage.
6
+ */
7
+
8
+ import type { NotionIntegrationConfig } from '../../types/integrations'
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ export interface NotionDatabase {
15
+ id: string
16
+ title: string
17
+ url: string
18
+ }
19
+
20
+ export interface NotionPage {
21
+ id: string
22
+ url: string
23
+ }
24
+
25
+ export interface NotionProperty {
26
+ type: string
27
+ [key: string]: unknown
28
+ }
29
+
30
+ export interface NotionDatabaseSchema {
31
+ title: string
32
+ properties: Record<string, NotionProperty>
33
+ }
34
+
35
+ export interface NotionQueryFilter {
36
+ property: string
37
+ rich_text?: { equals: string }
38
+ title?: { equals: string }
39
+ date?: { equals: string }
40
+ }
41
+
42
+ // Notion API response types
43
+ interface NotionApiResponse {
44
+ id?: string
45
+ url?: string
46
+ bot?: {
47
+ workspace_name?: string
48
+ owner?: {
49
+ workspace?: {
50
+ id?: string
51
+ }
52
+ }
53
+ }
54
+ results?: Array<{ id: string; properties: Record<string, unknown> }>
55
+ }
56
+
57
+ // =============================================================================
58
+ // Notion Client Class
59
+ // =============================================================================
60
+
61
+ export class NotionClient {
62
+ private config: NotionIntegrationConfig | null = null
63
+ private apiToken: string | null = null
64
+
65
+ /**
66
+ * Initialize client with config
67
+ */
68
+ initialize(config: NotionIntegrationConfig, apiToken?: string): void {
69
+ this.config = config
70
+ this.apiToken = apiToken || process.env.NOTION_TOKEN || null
71
+ }
72
+
73
+ /**
74
+ * Check if client is ready
75
+ */
76
+ isReady(): boolean {
77
+ return this.config?.enabled === true && this.apiToken !== null
78
+ }
79
+
80
+ /**
81
+ * Get workspace info
82
+ */
83
+ async getWorkspace(): Promise<{ id: string; name: string } | null> {
84
+ if (!this.apiToken) return null
85
+
86
+ try {
87
+ const response = await this.apiRequest('/users/me')
88
+ if (response.bot?.workspace_name) {
89
+ return {
90
+ id: response.bot.owner?.workspace?.id || 'unknown',
91
+ name: response.bot.workspace_name,
92
+ }
93
+ }
94
+ return null
95
+ } catch {
96
+ return null
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Create a database in Notion
102
+ */
103
+ async createDatabase(
104
+ parentPageId: string,
105
+ schema: NotionDatabaseSchema
106
+ ): Promise<NotionDatabase | null> {
107
+ try {
108
+ const response = await this.apiRequest('/databases', 'POST', {
109
+ parent: { type: 'page_id', page_id: parentPageId },
110
+ title: [{ type: 'text', text: { content: schema.title } }],
111
+ properties: schema.properties,
112
+ })
113
+
114
+ if (!response.id || !response.url) {
115
+ return null
116
+ }
117
+
118
+ return {
119
+ id: response.id,
120
+ title: schema.title,
121
+ url: response.url,
122
+ }
123
+ } catch (error) {
124
+ console.error('[notion] Failed to create database:', (error as Error).message)
125
+ return null
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Create a page in a database
131
+ */
132
+ async createPage(
133
+ databaseId: string,
134
+ properties: Record<string, unknown>
135
+ ): Promise<NotionPage | null> {
136
+ try {
137
+ const response = await this.apiRequest('/pages', 'POST', {
138
+ parent: { type: 'database_id', database_id: databaseId },
139
+ properties,
140
+ })
141
+
142
+ if (!response.id || !response.url) {
143
+ return null
144
+ }
145
+
146
+ return {
147
+ id: response.id,
148
+ url: response.url,
149
+ }
150
+ } catch (error) {
151
+ console.error('[notion] Failed to create page:', (error as Error).message)
152
+ return null
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Update a page
158
+ */
159
+ async updatePage(
160
+ pageId: string,
161
+ properties: Record<string, unknown>
162
+ ): Promise<NotionPage | null> {
163
+ try {
164
+ const response = await this.apiRequest(`/pages/${pageId}`, 'PATCH', {
165
+ properties,
166
+ })
167
+
168
+ if (!response.id || !response.url) {
169
+ return null
170
+ }
171
+
172
+ return {
173
+ id: response.id,
174
+ url: response.url,
175
+ }
176
+ } catch (error) {
177
+ console.error('[notion] Failed to update page:', (error as Error).message)
178
+ return null
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Query a database
184
+ */
185
+ async queryDatabase(
186
+ databaseId: string,
187
+ filter?: NotionQueryFilter
188
+ ): Promise<Array<{ id: string; properties: Record<string, unknown> }>> {
189
+ try {
190
+ const body: Record<string, unknown> = {}
191
+ if (filter) {
192
+ body.filter = filter
193
+ }
194
+
195
+ const response = await this.apiRequest(
196
+ `/databases/${databaseId}/query`,
197
+ 'POST',
198
+ body
199
+ )
200
+
201
+ return (response.results || []).map(
202
+ (page: { id: string; properties: Record<string, unknown> }) => ({
203
+ id: page.id,
204
+ properties: page.properties,
205
+ })
206
+ )
207
+ } catch (error) {
208
+ console.error('[notion] Failed to query database:', (error as Error).message)
209
+ return []
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Find page by project ID and name (for upsert)
215
+ */
216
+ async findPageByProjectAndName(
217
+ databaseId: string,
218
+ projectId: string,
219
+ name: string
220
+ ): Promise<string | null> {
221
+ try {
222
+ const response = await this.apiRequest(
223
+ `/databases/${databaseId}/query`,
224
+ 'POST',
225
+ {
226
+ filter: {
227
+ and: [
228
+ { property: 'Project', rich_text: { equals: projectId } },
229
+ { property: 'Name', title: { equals: name } },
230
+ ],
231
+ },
232
+ }
233
+ )
234
+
235
+ const results = response.results || []
236
+ return results.length > 0 ? results[0].id : null
237
+ } catch {
238
+ return null
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Create a page (for dashboard)
244
+ */
245
+ async createDashboardPage(
246
+ parentPageId: string,
247
+ title: string,
248
+ content: string
249
+ ): Promise<NotionPage | null> {
250
+ try {
251
+ const response = await this.apiRequest('/pages', 'POST', {
252
+ parent: { type: 'page_id', page_id: parentPageId },
253
+ properties: {
254
+ title: [{ type: 'text', text: { content: title } }],
255
+ },
256
+ children: [
257
+ {
258
+ object: 'block',
259
+ type: 'paragraph',
260
+ paragraph: {
261
+ rich_text: [{ type: 'text', text: { content } }],
262
+ },
263
+ },
264
+ ],
265
+ })
266
+
267
+ if (!response.id || !response.url) {
268
+ return null
269
+ }
270
+
271
+ return {
272
+ id: response.id,
273
+ url: response.url,
274
+ }
275
+ } catch (error) {
276
+ console.error('[notion] Failed to create page:', (error as Error).message)
277
+ return null
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Make API request to Notion
283
+ */
284
+ private async apiRequest(
285
+ endpoint: string,
286
+ method = 'GET',
287
+ body?: Record<string, unknown>
288
+ ): Promise<NotionApiResponse> {
289
+ if (!this.apiToken) {
290
+ throw new Error('Notion API token not configured')
291
+ }
292
+
293
+ const url = `https://api.notion.com/v1${endpoint}`
294
+ const headers: Record<string, string> = {
295
+ Authorization: `Bearer ${this.apiToken}`,
296
+ 'Content-Type': 'application/json',
297
+ 'Notion-Version': '2022-06-28',
298
+ }
299
+
300
+ const options: RequestInit = {
301
+ method,
302
+ headers,
303
+ }
304
+
305
+ if (body) {
306
+ options.body = JSON.stringify(body)
307
+ }
308
+
309
+ const response = await fetch(url, options)
310
+
311
+ if (!response.ok) {
312
+ const errorData = (await response.json().catch(() => ({}))) as { message?: string }
313
+ throw new Error(
314
+ `Notion API error: ${response.status} - ${errorData.message || 'Unknown error'}`
315
+ )
316
+ }
317
+
318
+ return (await response.json()) as NotionApiResponse
319
+ }
320
+ }
321
+
322
+ // Singleton instance
323
+ export const notionClient = new NotionClient()