ctxpkg 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +282 -0
- package/bin/cli.js +8 -0
- package/bin/daemon.js +7 -0
- package/package.json +70 -0
- package/src/agent/AGENTS.md +249 -0
- package/src/agent/agent.prompts.ts +66 -0
- package/src/agent/agent.test-runner.schemas.ts +158 -0
- package/src/agent/agent.test-runner.ts +436 -0
- package/src/agent/agent.ts +371 -0
- package/src/agent/agent.types.ts +94 -0
- package/src/backend/AGENTS.md +112 -0
- package/src/backend/backend.protocol.ts +95 -0
- package/src/backend/backend.schemas.ts +123 -0
- package/src/backend/backend.services.ts +151 -0
- package/src/backend/backend.ts +111 -0
- package/src/backend/backend.types.ts +34 -0
- package/src/cli/AGENTS.md +213 -0
- package/src/cli/cli.agent.ts +197 -0
- package/src/cli/cli.chat.ts +369 -0
- package/src/cli/cli.client.ts +55 -0
- package/src/cli/cli.collections.ts +491 -0
- package/src/cli/cli.config.ts +252 -0
- package/src/cli/cli.daemon.ts +160 -0
- package/src/cli/cli.documents.ts +413 -0
- package/src/cli/cli.mcp.ts +177 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/cli.utils.ts +122 -0
- package/src/client/AGENTS.md +135 -0
- package/src/client/client.adapters.ts +279 -0
- package/src/client/client.ts +86 -0
- package/src/client/client.types.ts +17 -0
- package/src/collections/AGENTS.md +185 -0
- package/src/collections/collections.schemas.ts +195 -0
- package/src/collections/collections.ts +1160 -0
- package/src/config/config.ts +118 -0
- package/src/daemon/AGENTS.md +168 -0
- package/src/daemon/daemon.config.ts +23 -0
- package/src/daemon/daemon.manager.ts +215 -0
- package/src/daemon/daemon.schemas.ts +22 -0
- package/src/daemon/daemon.ts +205 -0
- package/src/database/AGENTS.md +211 -0
- package/src/database/database.ts +64 -0
- package/src/database/migrations/migrations.001-init.ts +56 -0
- package/src/database/migrations/migrations.002-fts5.ts +32 -0
- package/src/database/migrations/migrations.ts +20 -0
- package/src/database/migrations/migrations.types.ts +9 -0
- package/src/documents/AGENTS.md +301 -0
- package/src/documents/documents.schemas.ts +190 -0
- package/src/documents/documents.ts +734 -0
- package/src/embedder/embedder.ts +53 -0
- package/src/exports.ts +0 -0
- package/src/mcp/AGENTS.md +264 -0
- package/src/mcp/mcp.ts +105 -0
- package/src/tools/AGENTS.md +228 -0
- package/src/tools/agent/agent.ts +45 -0
- package/src/tools/documents/documents.ts +401 -0
- package/src/tools/tools.langchain.ts +37 -0
- package/src/tools/tools.mcp.ts +46 -0
- package/src/tools/tools.types.ts +35 -0
- package/src/utils/utils.services.ts +46 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Collections — Agent Guidelines
|
|
2
|
+
|
|
3
|
+
This document describes the collections module architecture for AI agents working on this codebase.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The collections module manages context packages — local files, remote URLs, and git repositories. It handles project configuration (`context.json`), collection syncing, and manifest resolution. Think of it as the "package manager" part of ctxpkg.
|
|
8
|
+
|
|
9
|
+
## File Structure
|
|
10
|
+
|
|
11
|
+
| File | Purpose |
|
|
12
|
+
|------|---------|
|
|
13
|
+
| `collections.ts` | `CollectionsService` — sync logic, project config, manifest handling |
|
|
14
|
+
| `collections.schemas.ts` | Zod schemas for specs, manifests, and database records |
|
|
15
|
+
|
|
16
|
+
## Core Concepts
|
|
17
|
+
|
|
18
|
+
### Collection Spec
|
|
19
|
+
|
|
20
|
+
All collections are manifest-based packages identified by URL:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
type CollectionSpec = { url: string };
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Collection IDs are computed as `pkg:{normalized_url}`.
|
|
27
|
+
|
|
28
|
+
### Project Config (`context.json`)
|
|
29
|
+
|
|
30
|
+
Maps user-friendly names to collection specs (local to a project):
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"collections": {
|
|
35
|
+
"my-docs": { "url": "file://./docs/manifest.json" },
|
|
36
|
+
"langchain": { "url": "https://example.com/langchain/manifest.json" },
|
|
37
|
+
"react": { "url": "git+https://github.com/facebook/react#v18.2.0?manifest=docs/manifest.json" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### URL Formats
|
|
43
|
+
|
|
44
|
+
| Protocol | Format | Example |
|
|
45
|
+
|----------|--------|---------|
|
|
46
|
+
| Local file | `file://path/to/manifest.json` | `file://./docs/manifest.json` |
|
|
47
|
+
| HTTPS | `https://host/path/manifest.json` | `https://example.com/pkg/manifest.json` |
|
|
48
|
+
| Git HTTPS | `git+https://host/repo#ref?manifest=path` | `git+https://github.com/owner/repo#v1.0?manifest=docs/manifest.json` |
|
|
49
|
+
| Git SSH | `git+ssh://git@host/repo#ref?manifest=path` | `git+ssh://git@github.com/org/repo#main?manifest=manifest.json` |
|
|
50
|
+
| Git local | `git+file:///path/repo?manifest=path` | `git+file:///tmp/repo?manifest=manifest.json` |
|
|
51
|
+
| Bundle | `*.tar.gz` or `*.tgz` | `https://example.com/pkg.tar.gz` |
|
|
52
|
+
|
|
53
|
+
### Global Config (`~/.config/ctxpkg/global-context.json`)
|
|
54
|
+
|
|
55
|
+
Same structure as project config, but user-level (available across all projects):
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"collections": {
|
|
60
|
+
"typescript-docs": { "url": "https://example.com/ts-docs/manifest.json" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
When resolving collection aliases, local takes precedence over global.
|
|
66
|
+
|
|
67
|
+
### Package Manifests
|
|
68
|
+
|
|
69
|
+
Remote packages have a `manifest.json`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"name": "my-package",
|
|
74
|
+
"version": "1.0.0",
|
|
75
|
+
"baseUrl": "https://example.com/files/",
|
|
76
|
+
"sources": {
|
|
77
|
+
"files": [
|
|
78
|
+
"intro.md",
|
|
79
|
+
{ "path": "guide.md", "hash": "abc123..." }
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Sources can be `{ glob: [...] }` (local only) or `{ files: [...] }`.
|
|
86
|
+
|
|
87
|
+
## Architecture
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
91
|
+
│ CollectionsService │
|
|
92
|
+
├─────────────────────────────────────────────────────────────┤
|
|
93
|
+
│ Project Config │ Global Config │
|
|
94
|
+
│ ───────────────── │ ───────────────── │
|
|
95
|
+
│ readProjectConfig() │ readGlobalConfig() │
|
|
96
|
+
│ writeProjectConfig() │ writeGlobalConfig() │
|
|
97
|
+
│ projectConfigExists() │ globalConfigExists() │
|
|
98
|
+
├────────────────────────┼────────────────────────────────────┤
|
|
99
|
+
│ Unified Config Ops │ Sync Operations │
|
|
100
|
+
│ ───────────────── │ ──────────────── │
|
|
101
|
+
│ addToConfig() │ syncCollection() │
|
|
102
|
+
│ removeFromConfig() │ syncPkgCollection() │
|
|
103
|
+
│ getFromConfig() │ syncBundleCollection() │
|
|
104
|
+
│ getAllCollections() │ syncGitCollection() │
|
|
105
|
+
├────────────────────────┼────────────────────────────────────┤
|
|
106
|
+
│ Collection IDs │ Manifest Handling │
|
|
107
|
+
│ ───────────────── │ ──────────────────── │
|
|
108
|
+
│ computeCollectionId() │ loadLocalManifest() │
|
|
109
|
+
│ normalizePath() │ loadRemoteManifest() │
|
|
110
|
+
│ │ resolveManifestSources() │
|
|
111
|
+
│ │ downloadAndExtractBundle() │
|
|
112
|
+
└─────────────────────────────────────────────────────────────┘
|
|
113
|
+
│
|
|
114
|
+
▼
|
|
115
|
+
┌─────────────────┐
|
|
116
|
+
│ DocumentsService │ (stores documents)
|
|
117
|
+
└──────────────────┘
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Unified Config Operations:**
|
|
121
|
+
- `addToConfig(name, spec, { global })` - Add to project or global config
|
|
122
|
+
- `removeFromConfig(name, { global })` - Remove from project or global config
|
|
123
|
+
- `getFromConfig(name, { global })` - Get spec (if global undefined, searches local then global)
|
|
124
|
+
- `getAllCollections()` - Get all collections from both configs with source indicators
|
|
125
|
+
|
|
126
|
+
## Sync Flow
|
|
127
|
+
|
|
128
|
+
### Package Collections (file://, https://)
|
|
129
|
+
|
|
130
|
+
1. Parse manifest URL (file:// or https://)
|
|
131
|
+
2. Load and parse `manifest.json`
|
|
132
|
+
3. Check manifest hash — skip if unchanged
|
|
133
|
+
4. Resolve sources to file entries (expand globs or resolve paths)
|
|
134
|
+
5. Fetch and hash each file
|
|
135
|
+
6. Sync to database, update collection record
|
|
136
|
+
|
|
137
|
+
### Bundle Collections (.tar.gz)
|
|
138
|
+
|
|
139
|
+
1. Download and extract to temp directory
|
|
140
|
+
2. Find `manifest.json` in extracted content
|
|
141
|
+
3. Process as local package collection
|
|
142
|
+
4. Clean up temp directory
|
|
143
|
+
|
|
144
|
+
### Git Collections
|
|
145
|
+
|
|
146
|
+
1. Parse git URL to extract clone URL, ref, and manifest path
|
|
147
|
+
2. Clone to cwd-relative temp directory (`.ctxpkg/tmp/git-*`)
|
|
148
|
+
- Uses shallow clone (`--depth 1`) when possible
|
|
149
|
+
- Disables git hooks for security
|
|
150
|
+
- Preserves user's git config (includeIf directives, SSH keys, etc.)
|
|
151
|
+
3. Checkout specific ref (branch/tag/commit)
|
|
152
|
+
4. Load manifest from specified path in repo
|
|
153
|
+
5. Process as local package collection
|
|
154
|
+
6. Clean up temp directory
|
|
155
|
+
|
|
156
|
+
**Git URL Components:**
|
|
157
|
+
- `git+https://` or `git+ssh://` — protocol prefix
|
|
158
|
+
- `#ref` — optional branch, tag, or commit SHA (defaults to default branch)
|
|
159
|
+
- `?manifest=path` — required path to manifest.json in repo
|
|
160
|
+
|
|
161
|
+
## Collection ID Computation
|
|
162
|
+
|
|
163
|
+
IDs are deterministic and computed from the URL:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Normalized URL (trailing slashes removed)
|
|
167
|
+
`pkg:${url.replace(/\/+$/, '')}`
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This ensures the same spec always maps to the same collection ID.
|
|
171
|
+
|
|
172
|
+
## Key Patterns
|
|
173
|
+
|
|
174
|
+
### Manifest Source Resolution
|
|
175
|
+
|
|
176
|
+
The service handles multiple source formats:
|
|
177
|
+
|
|
178
|
+
- **Glob sources**: `{ glob: ['**/*.md'] }` — expanded relative to manifest directory
|
|
179
|
+
- **File sources**: `{ files: ['path.md', { url: '...' }] }` — resolved via `baseUrl` or manifest location
|
|
180
|
+
|
|
181
|
+
### Change Detection
|
|
182
|
+
|
|
183
|
+
- **Manifest hash**: Skip sync if manifest unchanged
|
|
184
|
+
- **Content hash**: Per-file content hash comparison for updates
|
|
185
|
+
- **Force sync**: `force: true` option bypasses hash checks
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// === Project Config (context.json) ===
|
|
4
|
+
|
|
5
|
+
const collectionSpecSchema = z.object({
|
|
6
|
+
url: z.string(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const projectConfigSchema = z.object({
|
|
10
|
+
collections: z.record(z.string(), collectionSpecSchema).default({}),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
type CollectionSpec = z.infer<typeof collectionSpecSchema>;
|
|
14
|
+
type ProjectConfig = z.infer<typeof projectConfigSchema>;
|
|
15
|
+
|
|
16
|
+
// === Package Manifest (manifest.json) ===
|
|
17
|
+
|
|
18
|
+
const globSourcesSchema = z.object({
|
|
19
|
+
glob: z.array(z.string()),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const fileEntryObjectSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
path: z.string().optional(),
|
|
25
|
+
url: z.string().optional(),
|
|
26
|
+
hash: z.string().optional(),
|
|
27
|
+
})
|
|
28
|
+
.refine((data) => (data.path && !data.url) || (!data.path && data.url), {
|
|
29
|
+
message: 'File entry must have either path or url, not both or neither',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const fileEntrySchema = z.union([z.string(), fileEntryObjectSchema]);
|
|
33
|
+
|
|
34
|
+
const fileSourcesSchema = z.object({
|
|
35
|
+
files: z.array(fileEntrySchema),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const manifestSourcesSchema = z.union([globSourcesSchema, fileSourcesSchema]);
|
|
39
|
+
|
|
40
|
+
const manifestSchema = z.object({
|
|
41
|
+
name: z.string(),
|
|
42
|
+
version: z.string(),
|
|
43
|
+
description: z.string().optional(),
|
|
44
|
+
baseUrl: z.string().optional(),
|
|
45
|
+
sources: manifestSourcesSchema,
|
|
46
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
type GlobSources = z.infer<typeof globSourcesSchema>;
|
|
50
|
+
type FileEntryObject = z.infer<typeof fileEntryObjectSchema>;
|
|
51
|
+
type FileEntry = z.infer<typeof fileEntrySchema>;
|
|
52
|
+
type FileSources = z.infer<typeof fileSourcesSchema>;
|
|
53
|
+
type ManifestSources = z.infer<typeof manifestSourcesSchema>;
|
|
54
|
+
type Manifest = z.infer<typeof manifestSchema>;
|
|
55
|
+
|
|
56
|
+
// === Database Record ===
|
|
57
|
+
|
|
58
|
+
const collectionRecordSchema = z.object({
|
|
59
|
+
id: z.string(),
|
|
60
|
+
url: z.string(),
|
|
61
|
+
name: z.string().nullable(),
|
|
62
|
+
version: z.string().nullable(),
|
|
63
|
+
description: z.string().nullable(),
|
|
64
|
+
manifest_hash: z.string().nullable(),
|
|
65
|
+
last_sync_at: z.string().nullable(),
|
|
66
|
+
created_at: z.string(),
|
|
67
|
+
updated_at: z.string(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
type CollectionRecord = z.infer<typeof collectionRecordSchema>;
|
|
71
|
+
|
|
72
|
+
// === Utility Types ===
|
|
73
|
+
|
|
74
|
+
type ResolvedFileEntry = {
|
|
75
|
+
id: string; // Document ID (path or URL)
|
|
76
|
+
url: string; // Resolved URL to fetch from
|
|
77
|
+
hash?: string; // Optional hash for change detection
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// === Helpers ===
|
|
81
|
+
|
|
82
|
+
const isGlobSources = (sources: ManifestSources): sources is GlobSources => {
|
|
83
|
+
return 'glob' in sources;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const isFileSources = (sources: ManifestSources): sources is FileSources => {
|
|
87
|
+
return 'files' in sources;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// === Git URL Parsing ===
|
|
91
|
+
|
|
92
|
+
type ParsedGitUrl = {
|
|
93
|
+
protocol: 'git';
|
|
94
|
+
cloneUrl: string; // URL to clone (without git+ prefix)
|
|
95
|
+
ref: string | null; // Branch, tag, or commit SHA
|
|
96
|
+
manifestPath: string; // Path to manifest within repo
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if a URL is a git URL (starts with git+https://, git+ssh://, or git+file://).
|
|
101
|
+
*/
|
|
102
|
+
const isGitUrl = (url: string): boolean => {
|
|
103
|
+
return url.startsWith('git+https://') || url.startsWith('git+ssh://') || url.startsWith('git+file://');
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse a git URL into its components.
|
|
108
|
+
*
|
|
109
|
+
* Format: git+<protocol>://<host>/<path>[#<ref>]?manifest=<path>
|
|
110
|
+
*
|
|
111
|
+
* Examples:
|
|
112
|
+
* git+https://github.com/owner/repo#v1.0.0?manifest=docs/manifest.json
|
|
113
|
+
* git+ssh://git@github.com/org/repo#main?manifest=manifest.json
|
|
114
|
+
*/
|
|
115
|
+
const parseGitUrl = (url: string): ParsedGitUrl => {
|
|
116
|
+
if (!isGitUrl(url)) {
|
|
117
|
+
throw new Error(`Not a git URL: ${url}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Remove git+ prefix
|
|
121
|
+
const urlWithoutPrefix = url.slice(4);
|
|
122
|
+
|
|
123
|
+
// Split off the fragment (#ref) first
|
|
124
|
+
const hashIndex = urlWithoutPrefix.indexOf('#');
|
|
125
|
+
const queryIndex = urlWithoutPrefix.indexOf('?');
|
|
126
|
+
|
|
127
|
+
let baseUrl: string;
|
|
128
|
+
let ref: string | null = null;
|
|
129
|
+
let queryString: string;
|
|
130
|
+
|
|
131
|
+
if (hashIndex !== -1 && (queryIndex === -1 || hashIndex < queryIndex)) {
|
|
132
|
+
// Has fragment: extract ref
|
|
133
|
+
baseUrl = urlWithoutPrefix.slice(0, hashIndex);
|
|
134
|
+
const afterHash = urlWithoutPrefix.slice(hashIndex + 1);
|
|
135
|
+
const refQueryIndex = afterHash.indexOf('?');
|
|
136
|
+
if (refQueryIndex !== -1) {
|
|
137
|
+
ref = afterHash.slice(0, refQueryIndex);
|
|
138
|
+
queryString = afterHash.slice(refQueryIndex + 1);
|
|
139
|
+
} else {
|
|
140
|
+
ref = afterHash;
|
|
141
|
+
queryString = '';
|
|
142
|
+
}
|
|
143
|
+
} else if (queryIndex !== -1) {
|
|
144
|
+
// No fragment, but has query
|
|
145
|
+
baseUrl = urlWithoutPrefix.slice(0, queryIndex);
|
|
146
|
+
queryString = urlWithoutPrefix.slice(queryIndex + 1);
|
|
147
|
+
} else {
|
|
148
|
+
throw new Error(`Git URL must specify manifest path: ?manifest=<path>`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Parse query string for manifest path
|
|
152
|
+
const params = new URLSearchParams(queryString);
|
|
153
|
+
const manifestPath = params.get('manifest');
|
|
154
|
+
|
|
155
|
+
if (!manifestPath) {
|
|
156
|
+
throw new Error(`Git URL must specify manifest path: ?manifest=<path>`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
protocol: 'git',
|
|
161
|
+
cloneUrl: baseUrl,
|
|
162
|
+
ref: ref || null,
|
|
163
|
+
manifestPath,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export type {
|
|
168
|
+
CollectionSpec,
|
|
169
|
+
ProjectConfig,
|
|
170
|
+
GlobSources,
|
|
171
|
+
FileEntryObject,
|
|
172
|
+
FileEntry,
|
|
173
|
+
FileSources,
|
|
174
|
+
ManifestSources,
|
|
175
|
+
Manifest,
|
|
176
|
+
CollectionRecord,
|
|
177
|
+
ResolvedFileEntry,
|
|
178
|
+
ParsedGitUrl,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export {
|
|
182
|
+
collectionSpecSchema,
|
|
183
|
+
projectConfigSchema,
|
|
184
|
+
globSourcesSchema,
|
|
185
|
+
fileEntryObjectSchema,
|
|
186
|
+
fileEntrySchema,
|
|
187
|
+
fileSourcesSchema,
|
|
188
|
+
manifestSourcesSchema,
|
|
189
|
+
manifestSchema,
|
|
190
|
+
collectionRecordSchema,
|
|
191
|
+
isGlobSources,
|
|
192
|
+
isFileSources,
|
|
193
|
+
isGitUrl,
|
|
194
|
+
parseGitUrl,
|
|
195
|
+
};
|