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.
- 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/integrations/notion/client.ts +323 -0
- package/core/integrations/notion/index.ts +43 -0
- package/core/integrations/notion/setup.ts +230 -0
- package/core/integrations/notion/sync.ts +311 -0
- package/core/integrations/notion/templates.ts +234 -0
- package/core/outcomes/analyzer.ts +7 -41
- package/core/outcomes/index.ts +1 -1
- package/core/outcomes/recorder.ts +1 -1
- package/core/plugin/builtin/notion.ts +178 -0
- 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 +70 -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 +396 -500
- package/core/types/infrastructure.ts +196 -0
- package/core/types/integrations.ts +57 -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/init.md +43 -0
- package/templates/commands/notion-setup.md +191 -0
- 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/mcp-config.json +28 -0
- 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
|
@@ -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
|
-
|
|
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()
|