@thecat69/cache-ctrl 1.0.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/README.md +558 -0
- package/cache_ctrl.ts +153 -0
- package/package.json +35 -0
- package/skills/cache-ctrl-caller/SKILL.md +154 -0
- package/skills/cache-ctrl-external/SKILL.md +130 -0
- package/skills/cache-ctrl-local/SKILL.md +213 -0
- package/src/cache/cacheManager.ts +241 -0
- package/src/cache/externalCache.ts +127 -0
- package/src/cache/localCache.ts +9 -0
- package/src/commands/checkFiles.ts +83 -0
- package/src/commands/checkFreshness.ts +123 -0
- package/src/commands/flush.ts +55 -0
- package/src/commands/inspect.ts +184 -0
- package/src/commands/install.ts +13 -0
- package/src/commands/invalidate.ts +53 -0
- package/src/commands/list.ts +83 -0
- package/src/commands/prune.ts +110 -0
- package/src/commands/search.ts +57 -0
- package/src/commands/touch.ts +47 -0
- package/src/commands/write.ts +170 -0
- package/src/files/changeDetector.ts +122 -0
- package/src/files/gitFiles.ts +41 -0
- package/src/files/openCodeInstaller.ts +66 -0
- package/src/http/freshnessChecker.ts +116 -0
- package/src/index.ts +557 -0
- package/src/search/keywordSearch.ts +59 -0
- package/src/types/cache.ts +91 -0
- package/src/types/commands.ts +192 -0
- package/src/types/result.ts +36 -0
- package/src/utils/fileStem.ts +7 -0
- package/src/utils/validate.ts +50 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { AgentType, ExternalCacheFile, LocalCacheFile } from "./cache.js";
|
|
2
|
+
|
|
3
|
+
// ── list ──────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface ListArgs {
|
|
6
|
+
agent?: AgentType | "all";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ListEntry {
|
|
10
|
+
file: string;
|
|
11
|
+
agent: AgentType;
|
|
12
|
+
subject: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
fetched_at: string;
|
|
15
|
+
age_human: string;
|
|
16
|
+
is_stale: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ListResult = { ok: true; value: ListEntry[] };
|
|
20
|
+
|
|
21
|
+
// ── inspect ───────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface InspectArgs {
|
|
24
|
+
agent: AgentType;
|
|
25
|
+
subject: string;
|
|
26
|
+
/** Path-keyword filter for local agent. Only facts entries whose file path contains
|
|
27
|
+
* at least one keyword (case-insensitive substring) are included. global_facts is
|
|
28
|
+
* always included. Ignored for external agent. */
|
|
29
|
+
filter?: string[];
|
|
30
|
+
/** Recursive folder prefix filter for local agent. Only facts entries whose file path
|
|
31
|
+
* equals the normalized folder or starts with `<normalizedFolder>/` are included.
|
|
32
|
+
* global_facts is always included. INVALID_ARGS if used with external agent. */
|
|
33
|
+
folder?: string;
|
|
34
|
+
/** Fact-content keyword filter for local agent. Only facts entries where at least one
|
|
35
|
+
* fact string contains at least one keyword (case-insensitive OR) are included.
|
|
36
|
+
* Silently ignored for external agent by design — external cache entries have no facts
|
|
37
|
+
* map. Use `folder` if you need to guard against passing search-facts to external; note
|
|
38
|
+
* that `folder` is an error on external while this is not. */
|
|
39
|
+
searchFacts?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type InspectResult = {
|
|
43
|
+
ok: true;
|
|
44
|
+
value: (ExternalCacheFile | LocalCacheFile) & {
|
|
45
|
+
file: string;
|
|
46
|
+
agent: AgentType;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ── flush ─────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export interface FlushArgs {
|
|
53
|
+
agent: AgentType | "all";
|
|
54
|
+
confirm: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type FlushResult = {
|
|
58
|
+
ok: true;
|
|
59
|
+
value: {
|
|
60
|
+
deleted: string[];
|
|
61
|
+
count: number;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ── invalidate ────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export interface InvalidateArgs {
|
|
68
|
+
agent: AgentType;
|
|
69
|
+
subject?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type InvalidateResult = {
|
|
73
|
+
ok: true;
|
|
74
|
+
value: {
|
|
75
|
+
invalidated: string[];
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ── touch ─────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export interface TouchArgs {
|
|
82
|
+
agent: AgentType;
|
|
83
|
+
subject?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type TouchResult = {
|
|
87
|
+
ok: true;
|
|
88
|
+
value: {
|
|
89
|
+
touched: string[];
|
|
90
|
+
new_timestamp: string;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ── prune ─────────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
export interface PruneArgs {
|
|
97
|
+
agent?: AgentType | "all";
|
|
98
|
+
maxAge?: string;
|
|
99
|
+
delete?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type PruneResult = {
|
|
103
|
+
ok: true;
|
|
104
|
+
value: {
|
|
105
|
+
matched: Array<{ file: string; agent: AgentType; subject: string }>;
|
|
106
|
+
action: "invalidated" | "deleted";
|
|
107
|
+
count: number;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ── check-freshness ───────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export interface CheckFreshnessArgs {
|
|
114
|
+
subject: string;
|
|
115
|
+
url?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type CheckFreshnessResult = {
|
|
119
|
+
ok: true;
|
|
120
|
+
value: {
|
|
121
|
+
subject: string;
|
|
122
|
+
sources: Array<{
|
|
123
|
+
url: string;
|
|
124
|
+
status: "fresh" | "stale" | "error";
|
|
125
|
+
http_status?: number;
|
|
126
|
+
error?: string;
|
|
127
|
+
}>;
|
|
128
|
+
overall: "fresh" | "stale" | "error";
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// ── check-files ───────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
export type CheckFilesResult = {
|
|
135
|
+
ok: true;
|
|
136
|
+
value: {
|
|
137
|
+
status: "changed" | "unchanged";
|
|
138
|
+
changed_files: Array<{
|
|
139
|
+
path: string;
|
|
140
|
+
reason: "mtime" | "hash" | "missing";
|
|
141
|
+
}>;
|
|
142
|
+
unchanged_files: string[];
|
|
143
|
+
missing_files: string[];
|
|
144
|
+
new_files: string[];
|
|
145
|
+
deleted_git_files: string[];
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// ── search ────────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
export interface SearchArgs {
|
|
152
|
+
keywords: string[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type SearchResult = {
|
|
156
|
+
ok: true;
|
|
157
|
+
value: Array<{
|
|
158
|
+
file: string;
|
|
159
|
+
subject: string;
|
|
160
|
+
description?: string;
|
|
161
|
+
agent: AgentType;
|
|
162
|
+
fetched_at: string;
|
|
163
|
+
score: number;
|
|
164
|
+
}>;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ── write ─────────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
export interface WriteArgs {
|
|
170
|
+
agent: AgentType;
|
|
171
|
+
subject?: string; // required for external, unused for local
|
|
172
|
+
content: Record<string, unknown>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type WriteResult = {
|
|
176
|
+
ok: true;
|
|
177
|
+
value: {
|
|
178
|
+
file: string;
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// ── install ───────────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
export interface InstallArgs {
|
|
185
|
+
configDir?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface InstallResult {
|
|
189
|
+
toolPath: string;
|
|
190
|
+
skillPaths: string[];
|
|
191
|
+
configDir: string;
|
|
192
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export enum ErrorCode {
|
|
2
|
+
// File system errors
|
|
3
|
+
FILE_NOT_FOUND = "FILE_NOT_FOUND",
|
|
4
|
+
FILE_READ_ERROR = "FILE_READ_ERROR",
|
|
5
|
+
FILE_WRITE_ERROR = "FILE_WRITE_ERROR",
|
|
6
|
+
PARSE_ERROR = "PARSE_ERROR",
|
|
7
|
+
|
|
8
|
+
// Lock errors
|
|
9
|
+
LOCK_TIMEOUT = "LOCK_TIMEOUT",
|
|
10
|
+
LOCK_ERROR = "LOCK_ERROR",
|
|
11
|
+
|
|
12
|
+
// Validation errors
|
|
13
|
+
INVALID_ARGS = "INVALID_ARGS",
|
|
14
|
+
CONFIRMATION_REQUIRED = "CONFIRMATION_REQUIRED",
|
|
15
|
+
VALIDATION_ERROR = "VALIDATION_ERROR",
|
|
16
|
+
|
|
17
|
+
// Search/match errors
|
|
18
|
+
NO_MATCH = "NO_MATCH",
|
|
19
|
+
AMBIGUOUS_MATCH = "AMBIGUOUS_MATCH",
|
|
20
|
+
|
|
21
|
+
// HTTP errors
|
|
22
|
+
HTTP_REQUEST_FAILED = "HTTP_REQUEST_FAILED",
|
|
23
|
+
URL_NOT_FOUND = "URL_NOT_FOUND",
|
|
24
|
+
|
|
25
|
+
// Internal
|
|
26
|
+
UNKNOWN = "UNKNOWN",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CacheError {
|
|
30
|
+
code: ErrorCode;
|
|
31
|
+
error: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type Result<T, E extends CacheError = CacheError> =
|
|
35
|
+
| { ok: true; value: T }
|
|
36
|
+
| { ok: false; error: string; code: E["code"] };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
|
|
3
|
+
/** Returns the file name without its `.json` extension, or the name as-is if not `.json`. */
|
|
4
|
+
export function getFileStem(filePath: string): string {
|
|
5
|
+
const name = basename(filePath);
|
|
6
|
+
return name.endsWith(".json") ? name.slice(0, -5) : name;
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ZodError } from "zod";
|
|
2
|
+
|
|
3
|
+
import { ErrorCode, type Result } from "../types/result.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regex for safe cache subject names.
|
|
7
|
+
* First character must be alphanumeric — blocks pure-dot strings and dot-leading strings
|
|
8
|
+
* that would otherwise enable relative path traversal (e.g. "../secrets", "..").
|
|
9
|
+
*/
|
|
10
|
+
const SUBJECT_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
11
|
+
|
|
12
|
+
/** Maximum allowed length for a cache subject. */
|
|
13
|
+
const SUBJECT_MAX_LENGTH = 128;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Formats a ZodError's issues into a human-readable semicolon-separated string.
|
|
17
|
+
* Each issue is prefixed with its dot-separated field path when present.
|
|
18
|
+
*/
|
|
19
|
+
export function formatZodError(error: ZodError): string {
|
|
20
|
+
return error.issues
|
|
21
|
+
.map((i) => {
|
|
22
|
+
if (i.path.length === 0) return i.message;
|
|
23
|
+
const pathStr = i.path.map((seg) => String(seg).replace(/[\x00-\x1f\x7f]/g, "?")).join(".");
|
|
24
|
+
return `${pathStr}: ${i.message}`;
|
|
25
|
+
})
|
|
26
|
+
.join("; ");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validates a cache subject string.
|
|
31
|
+
* Rejects values that could enable path traversal (e.g. "../secrets") or inject
|
|
32
|
+
* unexpected characters into file paths derived from the subject.
|
|
33
|
+
*/
|
|
34
|
+
export function validateSubject(subject: string): Result<void> {
|
|
35
|
+
if (subject.length > SUBJECT_MAX_LENGTH) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
error: "Subject must be 128 characters or fewer",
|
|
39
|
+
code: ErrorCode.INVALID_ARGS,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!SUBJECT_PATTERN.test(subject)) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
error: `Invalid subject "${subject}": must match /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/`,
|
|
46
|
+
code: ErrorCode.INVALID_ARGS,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, value: undefined };
|
|
50
|
+
}
|