@openparachute/vault 0.3.3 → 0.4.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/.parachute/module.json +15 -0
- package/core/src/core.test.ts +2252 -7
- package/core/src/links.ts +1 -1
- package/core/src/mcp.ts +801 -67
- package/core/src/note-schemas.ts +232 -0
- package/core/src/notes.ts +313 -35
- package/core/src/obsidian.ts +3 -3
- package/core/src/paths.ts +1 -1
- package/core/src/query-operators.ts +23 -7
- package/core/src/schema-defaults.ts +287 -0
- package/core/src/schema.ts +393 -9
- package/core/src/store.ts +248 -6
- package/core/src/tag-hierarchy.ts +137 -0
- package/core/src/tag-schemas.ts +242 -42
- package/core/src/types.ts +100 -6
- package/core/src/wikilinks.ts +3 -3
- package/package.json +13 -3
- package/src/admin-spa.test.ts +161 -0
- package/src/admin-spa.ts +161 -0
- package/src/auth-hub-jwt.test.ts +231 -0
- package/src/auth-status.ts +84 -0
- package/src/auth.test.ts +135 -23
- package/src/auth.ts +144 -15
- package/src/backup.ts +4 -7
- package/src/cli.ts +322 -57
- package/src/config.test.ts +44 -0
- package/src/config.ts +68 -40
- package/src/hub-jwt.test.ts +296 -0
- package/src/hub-jwt.ts +79 -0
- package/src/init.test.ts +216 -0
- package/src/mcp-http.ts +30 -28
- package/src/mcp-install.ts +1 -1
- package/src/mcp-tools.ts +294 -6
- package/src/module-config.ts +1 -1
- package/src/oauth.test.ts +345 -0
- package/src/oauth.ts +85 -14
- package/src/owner-auth.ts +57 -1
- package/src/prompt.ts +6 -5
- package/src/routes.ts +686 -58
- package/src/routing.test.ts +466 -1
- package/src/routing.ts +108 -24
- package/src/scopes.test.ts +66 -8
- package/src/scopes.ts +163 -37
- package/src/server.ts +24 -2
- package/src/services-manifest.test.ts +20 -0
- package/src/services-manifest.ts +9 -2
- package/src/stop-signal.test.ts +85 -0
- package/src/storage.test.ts +92 -0
- package/src/tag-scope.ts +118 -0
- package/src/token-store.test.ts +47 -0
- package/src/token-store.ts +128 -13
- package/src/tokens-routes.test.ts +720 -0
- package/src/tokens-routes.ts +392 -0
- package/src/transcription-worker.test.ts +5 -0
- package/src/triggers.ts +1 -1
- package/src/two-factor.ts +2 -2
- package/src/vault-create.test.ts +193 -0
- package/src/vault-name.test.ts +123 -0
- package/src/vault-name.ts +80 -0
- package/src/vault.test.ts +868 -3
- package/tsconfig.json +8 -1
- package/.claude/settings.local.json +0 -8
- package/.dockerignore +0 -8
- package/.env.example +0 -9
- package/CHANGELOG.md +0 -175
- package/CLAUDE.md +0 -125
- package/Caddyfile +0 -3
- package/Dockerfile +0 -22
- package/bun.lock +0 -219
- package/bunfig.toml +0 -2
- package/deploy/parachute-vault.service +0 -20
- package/docker-compose.yml +0 -50
- package/docs/HTTP_API.md +0 -434
- package/docs/auth-model.md +0 -340
- package/fly.toml +0 -24
- package/package/package.json +0 -32
- package/railway.json +0 -14
- package/scripts/migrate-audio-to-opus.test.ts +0 -237
- package/scripts/migrate-audio-to-opus.ts +0 -499
package/core/src/tag-schemas.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tag
|
|
2
|
+
* Tag record CRUD — DB-backed storage for the per-tag identity row.
|
|
3
3
|
*
|
|
4
|
-
* Each tag
|
|
5
|
-
* fields
|
|
6
|
-
* and
|
|
4
|
+
* Each tag row carries: human-readable description, indexed metadata field
|
|
5
|
+
* declarations (`fields`), typed-link relationship declarations
|
|
6
|
+
* (`relationships`), and the hierarchy parent list (`parent_names`).
|
|
7
|
+
* See parachute-patterns/patterns/tag-data-model.md.
|
|
8
|
+
*
|
|
9
|
+
* This module retains the historical `tag-schemas` filename and exports
|
|
10
|
+
* (`TagSchema`, `listTagSchemas`, `getTagSchema`, `upsertTagSchema`,
|
|
11
|
+
* `deleteTagSchema`, `getTagSchemaMap`) as a thin schema-only facade —
|
|
12
|
+
* callers that only care about `description + fields` keep working without
|
|
13
|
+
* change. New surface (`TagRecord`, `listTagRecords`, `getTagRecord`,
|
|
14
|
+
* `upsertTagRecord`) returns the full row including relationships and
|
|
15
|
+
* parent_names.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
import { Database } from "bun:sqlite";
|
|
@@ -23,87 +32,278 @@ export interface TagFieldSchema {
|
|
|
23
32
|
indexed?: boolean;
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Cardinality vocabulary for typed relationships. Names rather than
|
|
37
|
+
* algebra so AI clients reading `list-tags` can reason about intent
|
|
38
|
+
* directly. Phase 1 is informational — declarations are not enforced
|
|
39
|
+
* at write time. See patterns/tag-data-model.md §Typed relationships.
|
|
40
|
+
*/
|
|
41
|
+
export type TagRelCardinality = "one" | "optional" | "many" | "many-required";
|
|
42
|
+
|
|
43
|
+
export const TAG_REL_CARDINALITIES: readonly TagRelCardinality[] = [
|
|
44
|
+
"one",
|
|
45
|
+
"optional",
|
|
46
|
+
"many",
|
|
47
|
+
"many-required",
|
|
48
|
+
] as const;
|
|
49
|
+
|
|
50
|
+
export interface TagRelationship {
|
|
51
|
+
target_tag: string;
|
|
52
|
+
cardinality: TagRelCardinality;
|
|
53
|
+
description?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Schema-only view of a tag — the historical shape. Backwards-compatible
|
|
58
|
+
* with v13-and-earlier callers.
|
|
59
|
+
*/
|
|
26
60
|
export interface TagSchema {
|
|
27
61
|
tag: string;
|
|
28
62
|
description?: string;
|
|
29
63
|
fields?: Record<string, TagFieldSchema>;
|
|
30
64
|
}
|
|
31
65
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Full tag record — schema + typed relationships + hierarchy parents.
|
|
68
|
+
*/
|
|
69
|
+
export interface TagRecord extends TagSchema {
|
|
70
|
+
relationships?: Record<string, TagRelationship>;
|
|
71
|
+
parent_names?: string[];
|
|
72
|
+
created_at?: string;
|
|
73
|
+
updated_at?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// DB row shape (post-v14 `tags` table).
|
|
77
|
+
interface TagRow {
|
|
78
|
+
name: string;
|
|
35
79
|
description: string | null;
|
|
36
80
|
fields: string | null;
|
|
81
|
+
relationships: string | null;
|
|
82
|
+
parent_names: string | null;
|
|
83
|
+
created_at: string | null;
|
|
84
|
+
updated_at: string | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// CRUD — full record
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/** List all tag records, sorted by name. */
|
|
92
|
+
export function listTagRecords(db: Database): TagRecord[] {
|
|
93
|
+
const rows = db.prepare(
|
|
94
|
+
"SELECT name, description, fields, relationships, parent_names, created_at, updated_at FROM tags ORDER BY name",
|
|
95
|
+
).all() as TagRow[];
|
|
96
|
+
return rows.map(rowToRecord);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Get a single tag record, or null if the tag doesn't exist. */
|
|
100
|
+
export function getTagRecord(db: Database, tag: string): TagRecord | null {
|
|
101
|
+
const row = db.prepare(
|
|
102
|
+
"SELECT name, description, fields, relationships, parent_names, created_at, updated_at FROM tags WHERE name = ?",
|
|
103
|
+
).get(tag) as TagRow | undefined;
|
|
104
|
+
return row ? rowToRecord(row) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Upsert a tag record — partial update. Any field left `undefined` is
|
|
109
|
+
* preserved. Pass `null` explicitly to clear a column. Always touches
|
|
110
|
+
* `updated_at`; sets `created_at` on first insert.
|
|
111
|
+
*/
|
|
112
|
+
export function upsertTagRecord(
|
|
113
|
+
db: Database,
|
|
114
|
+
tag: string,
|
|
115
|
+
patch: {
|
|
116
|
+
description?: string | null;
|
|
117
|
+
fields?: Record<string, TagFieldSchema> | null;
|
|
118
|
+
relationships?: Record<string, TagRelationship> | null;
|
|
119
|
+
parent_names?: string[] | null;
|
|
120
|
+
},
|
|
121
|
+
): TagRecord {
|
|
122
|
+
const now = new Date().toISOString();
|
|
123
|
+
db.prepare(
|
|
124
|
+
"INSERT OR IGNORE INTO tags (name, created_at, updated_at) VALUES (?, ?, ?)",
|
|
125
|
+
).run(tag, now, now);
|
|
126
|
+
|
|
127
|
+
const existing = getTagRecord(db, tag);
|
|
128
|
+
|
|
129
|
+
const description =
|
|
130
|
+
patch.description === undefined ? (existing?.description ?? null) : patch.description;
|
|
131
|
+
const fields =
|
|
132
|
+
patch.fields === undefined
|
|
133
|
+
? jsonOrNull(existing?.fields)
|
|
134
|
+
: jsonOrNull(patch.fields);
|
|
135
|
+
const relationships =
|
|
136
|
+
patch.relationships === undefined
|
|
137
|
+
? jsonOrNull(existing?.relationships)
|
|
138
|
+
: jsonOrNull(patch.relationships);
|
|
139
|
+
const parent_names =
|
|
140
|
+
patch.parent_names === undefined
|
|
141
|
+
? jsonOrNull(existing?.parent_names)
|
|
142
|
+
: jsonOrNull(patch.parent_names);
|
|
143
|
+
|
|
144
|
+
db.prepare(
|
|
145
|
+
`UPDATE tags
|
|
146
|
+
SET description = ?, fields = ?, relationships = ?, parent_names = ?, updated_at = ?
|
|
147
|
+
WHERE name = ?`,
|
|
148
|
+
).run(description, fields, relationships, parent_names, now, tag);
|
|
149
|
+
|
|
150
|
+
return getTagRecord(db, tag)!;
|
|
37
151
|
}
|
|
38
152
|
|
|
39
153
|
// ---------------------------------------------------------------------------
|
|
40
|
-
// CRUD
|
|
154
|
+
// CRUD — schema-only facade (back-compat)
|
|
41
155
|
// ---------------------------------------------------------------------------
|
|
42
156
|
|
|
43
|
-
/** List
|
|
157
|
+
/** List schema-only views for tags that have a description or fields set. */
|
|
44
158
|
export function listTagSchemas(db: Database): TagSchema[] {
|
|
45
|
-
const rows = db.prepare(
|
|
46
|
-
|
|
159
|
+
const rows = db.prepare(
|
|
160
|
+
"SELECT name, description, fields FROM tags WHERE description IS NOT NULL OR fields IS NOT NULL ORDER BY name",
|
|
161
|
+
).all() as { name: string; description: string | null; fields: string | null }[];
|
|
162
|
+
return rows.map((r) => ({
|
|
163
|
+
tag: r.name,
|
|
164
|
+
description: r.description ?? undefined,
|
|
165
|
+
fields: parseJson<Record<string, TagFieldSchema>>(r.fields),
|
|
166
|
+
}));
|
|
47
167
|
}
|
|
48
168
|
|
|
49
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* Schema-only view for a single tag. Returns null if the tag has neither
|
|
171
|
+
* a description nor fields (matches v13 behavior, where the absence of a
|
|
172
|
+
* `tag_schemas` row meant "no schema").
|
|
173
|
+
*/
|
|
50
174
|
export function getTagSchema(db: Database, tag: string): TagSchema | null {
|
|
51
|
-
const row = db.prepare(
|
|
52
|
-
|
|
175
|
+
const row = db.prepare(
|
|
176
|
+
"SELECT name, description, fields FROM tags WHERE name = ?",
|
|
177
|
+
).get(tag) as { name: string; description: string | null; fields: string | null } | undefined;
|
|
178
|
+
if (!row) return null;
|
|
179
|
+
if (row.description === null && row.fields === null) return null;
|
|
180
|
+
return {
|
|
181
|
+
tag: row.name,
|
|
182
|
+
description: row.description ?? undefined,
|
|
183
|
+
fields: parseJson<Record<string, TagFieldSchema>>(row.fields),
|
|
184
|
+
};
|
|
53
185
|
}
|
|
54
186
|
|
|
55
187
|
/** Get all schemas as a lookup map (tag → schema). Used by schema effects. */
|
|
56
|
-
export function getTagSchemaMap(
|
|
57
|
-
|
|
188
|
+
export function getTagSchemaMap(
|
|
189
|
+
db: Database,
|
|
190
|
+
): Record<string, { description?: string; fields?: Record<string, TagFieldSchema> }> {
|
|
58
191
|
const map: Record<string, { description?: string; fields?: Record<string, TagFieldSchema> }> = {};
|
|
59
|
-
for (const s of
|
|
192
|
+
for (const s of listTagSchemas(db)) {
|
|
60
193
|
map[s.tag] = { description: s.description, fields: s.fields };
|
|
61
194
|
}
|
|
62
195
|
return map;
|
|
63
196
|
}
|
|
64
197
|
|
|
65
198
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
199
|
+
* Set or replace a tag's schema (description + fields). Other columns
|
|
200
|
+
* (`relationships`, `parent_names`) are left untouched. Idempotent.
|
|
68
201
|
*/
|
|
69
202
|
export function upsertTagSchema(
|
|
70
203
|
db: Database,
|
|
71
204
|
tag: string,
|
|
72
205
|
schema: { description?: string; fields?: Record<string, TagFieldSchema> },
|
|
73
206
|
): TagSchema {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
db.
|
|
79
|
-
INSERT INTO tag_schemas (tag_name, description, fields)
|
|
80
|
-
VALUES (?, ?, ?)
|
|
81
|
-
ON CONFLICT(tag_name) DO UPDATE SET
|
|
82
|
-
description = excluded.description,
|
|
83
|
-
fields = excluded.fields
|
|
84
|
-
`).run(tag, schema.description ?? null, fieldsJson);
|
|
85
|
-
|
|
86
|
-
return getTagSchema(db, tag)!;
|
|
207
|
+
upsertTagRecord(db, tag, {
|
|
208
|
+
description: schema.description ?? null,
|
|
209
|
+
fields: schema.fields ?? null,
|
|
210
|
+
});
|
|
211
|
+
return getTagSchema(db, tag) ?? { tag, description: schema.description, fields: schema.fields };
|
|
87
212
|
}
|
|
88
213
|
|
|
89
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* Clear a tag's schema (description + fields). Returns true if anything
|
|
216
|
+
* was cleared. Other columns and the tag row itself are preserved — to
|
|
217
|
+
* delete the tag entirely, use `noteOps.deleteTag`.
|
|
218
|
+
*/
|
|
90
219
|
export function deleteTagSchema(db: Database, tag: string): boolean {
|
|
91
|
-
const
|
|
92
|
-
|
|
220
|
+
const before = getTagSchema(db, tag);
|
|
221
|
+
if (!before) return false;
|
|
222
|
+
db.prepare(
|
|
223
|
+
"UPDATE tags SET description = NULL, fields = NULL, updated_at = ? WHERE name = ?",
|
|
224
|
+
).run(new Date().toISOString(), tag);
|
|
225
|
+
return true;
|
|
93
226
|
}
|
|
94
227
|
|
|
95
228
|
// ---------------------------------------------------------------------------
|
|
96
|
-
//
|
|
229
|
+
// Validation — typed relationships
|
|
97
230
|
// ---------------------------------------------------------------------------
|
|
98
231
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Validate a `relationships` payload before persisting. Returns the
|
|
234
|
+
* canonicalized object on success; throws Error with a user-readable
|
|
235
|
+
* message on the first violation. Rules:
|
|
236
|
+
*
|
|
237
|
+
* - Each value must declare `target_tag` (non-empty string) and
|
|
238
|
+
* `cardinality` from the named vocabulary.
|
|
239
|
+
* - `description` is optional, must be a string when present.
|
|
240
|
+
* - Relationship keys must be non-empty strings.
|
|
241
|
+
*/
|
|
242
|
+
export function validateRelationships(
|
|
243
|
+
raw: unknown,
|
|
244
|
+
): Record<string, TagRelationship> {
|
|
245
|
+
if (raw === null || raw === undefined) {
|
|
246
|
+
throw new Error("relationships: expected an object, got null/undefined");
|
|
247
|
+
}
|
|
248
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
249
|
+
throw new Error("relationships: expected an object mapping rel name → declaration");
|
|
103
250
|
}
|
|
251
|
+
const out: Record<string, TagRelationship> = {};
|
|
252
|
+
for (const [rel, decl] of Object.entries(raw as Record<string, unknown>)) {
|
|
253
|
+
if (!rel || typeof rel !== "string") {
|
|
254
|
+
throw new Error("relationships: keys must be non-empty strings");
|
|
255
|
+
}
|
|
256
|
+
if (!decl || typeof decl !== "object" || Array.isArray(decl)) {
|
|
257
|
+
throw new Error(`relationships["${rel}"]: declaration must be an object`);
|
|
258
|
+
}
|
|
259
|
+
const d = decl as Record<string, unknown>;
|
|
260
|
+
if (typeof d.target_tag !== "string" || d.target_tag.length === 0) {
|
|
261
|
+
throw new Error(`relationships["${rel}"]: target_tag must be a non-empty string`);
|
|
262
|
+
}
|
|
263
|
+
const card = d.cardinality;
|
|
264
|
+
if (typeof card !== "string" || !TAG_REL_CARDINALITIES.includes(card as TagRelCardinality)) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`relationships["${rel}"]: cardinality must be one of ${TAG_REL_CARDINALITIES.join(" | ")}; got ${JSON.stringify(card)}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (d.description !== undefined && typeof d.description !== "string") {
|
|
270
|
+
throw new Error(`relationships["${rel}"]: description must be a string when set`);
|
|
271
|
+
}
|
|
272
|
+
out[rel] = {
|
|
273
|
+
target_tag: d.target_tag,
|
|
274
|
+
cardinality: card as TagRelCardinality,
|
|
275
|
+
...(d.description !== undefined ? { description: d.description as string } : {}),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Helpers
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
function rowToRecord(row: TagRow): TagRecord {
|
|
104
286
|
return {
|
|
105
|
-
tag: row.
|
|
287
|
+
tag: row.name,
|
|
106
288
|
description: row.description ?? undefined,
|
|
107
|
-
fields,
|
|
289
|
+
fields: parseJson<Record<string, TagFieldSchema>>(row.fields),
|
|
290
|
+
relationships: parseJson<Record<string, TagRelationship>>(row.relationships),
|
|
291
|
+
parent_names: parseJson<string[]>(row.parent_names),
|
|
292
|
+
created_at: row.created_at ?? undefined,
|
|
293
|
+
updated_at: row.updated_at ?? undefined,
|
|
108
294
|
};
|
|
109
295
|
}
|
|
296
|
+
|
|
297
|
+
function parseJson<T>(raw: string | null): T | undefined {
|
|
298
|
+
if (raw === null || raw === undefined) return undefined;
|
|
299
|
+
try { return JSON.parse(raw) as T; } catch { return undefined; }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function jsonOrNull(value: unknown): string | null {
|
|
303
|
+
if (value === null || value === undefined) return null;
|
|
304
|
+
if (typeof value === "string") {
|
|
305
|
+
// Already-encoded payload (e.g. when copying from an existing row).
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
return JSON.stringify(value);
|
|
309
|
+
}
|
package/core/src/types.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
import type { TagFieldSchema, TagRelationship, TagRecord } from "./tag-schemas.js";
|
|
2
|
+
import type {
|
|
3
|
+
NoteSchemaField,
|
|
4
|
+
NoteSchemaRecord,
|
|
5
|
+
NoteSchemaPatch,
|
|
6
|
+
SchemaMappingKind,
|
|
7
|
+
SchemaMappingRecord,
|
|
8
|
+
ListMappingsOpts,
|
|
9
|
+
} from "./note-schemas.js";
|
|
10
|
+
|
|
11
|
+
// ---- Re-exports ----
|
|
12
|
+
|
|
13
|
+
export type { TagFieldSchema, TagRelationship, TagRecord } from "./tag-schemas.js";
|
|
14
|
+
export type {
|
|
15
|
+
NoteSchemaField,
|
|
16
|
+
NoteSchemaRecord,
|
|
17
|
+
NoteSchemaPatch,
|
|
18
|
+
SchemaMappingKind,
|
|
19
|
+
SchemaMappingRecord,
|
|
20
|
+
} from "./note-schemas.js";
|
|
21
|
+
|
|
1
22
|
// ---- Note ----
|
|
2
23
|
|
|
3
24
|
export interface Note {
|
|
@@ -41,6 +62,7 @@ export interface VaultStats {
|
|
|
41
62
|
notesByMonth: { month: string; count: number }[];
|
|
42
63
|
topTags: { tag: string; count: number }[];
|
|
43
64
|
tagCount: number;
|
|
65
|
+
attachmentCount: number;
|
|
44
66
|
linkCount: number;
|
|
45
67
|
}
|
|
46
68
|
|
|
@@ -57,14 +79,33 @@ export interface QueryOpts {
|
|
|
57
79
|
hasLinks?: boolean;
|
|
58
80
|
path?: string; // exact path match (case-insensitive)
|
|
59
81
|
pathPrefix?: string; // e.g., "Projects/Parachute" matches "Projects/Parachute/README"
|
|
82
|
+
// Restrict results to a specific set of note IDs. The MCP `near` query uses
|
|
83
|
+
// this to push graph-neighborhood scoping into the SQL WHERE clause so that
|
|
84
|
+
// LIMIT and ORDER BY apply to the filtered set, not the whole notes table.
|
|
85
|
+
// Empty array → no rows match (avoids `IN ()` syntax error).
|
|
86
|
+
ids?: string[];
|
|
60
87
|
// Per-field metadata filter. Each value is either a primitive (exact
|
|
61
88
|
// match, today's behavior) or an operator object — `{ eq, ne, gt, gte, lt,
|
|
62
89
|
// lte, in, not_in, exists }` — which routes through the generated column
|
|
63
90
|
// for the field. Operator queries require the field to be declared
|
|
64
91
|
// `indexed: true` in a tag schema; undeclared fields error loudly.
|
|
65
92
|
metadata?: Record<string, unknown>;
|
|
93
|
+
// Legacy shorthand: filters on `n.created_at` (vault ingestion time).
|
|
94
|
+
// Equivalent to `dateFilter: { field: "created_at", from, to }`. Kept
|
|
95
|
+
// as the common path; specifying both this and `dateFilter` rejects.
|
|
66
96
|
dateFrom?: string; // ISO date
|
|
67
97
|
dateTo?: string; // ISO date
|
|
98
|
+
// Generalized date range. `field` defaults to `created_at`; any other
|
|
99
|
+
// field must be declared `indexed: true` in a tag schema (so the SQL
|
|
100
|
+
// hits a real B-tree index, same contract as `metadata` operator
|
|
101
|
+
// queries and `orderBy`). Use this to filter on a *content* date — an
|
|
102
|
+
// email's received date, a meeting's scheduled date — rather than the
|
|
103
|
+
// ingestion timestamp.
|
|
104
|
+
dateFilter?: {
|
|
105
|
+
field?: string;
|
|
106
|
+
from?: string;
|
|
107
|
+
to?: string;
|
|
108
|
+
};
|
|
68
109
|
sort?: "asc" | "desc";
|
|
69
110
|
// Sort by an indexed metadata field instead of `created_at`. Must be
|
|
70
111
|
// declared `indexed: true`; errors loudly otherwise. Direction is taken
|
|
@@ -114,7 +155,7 @@ export interface Store {
|
|
|
114
155
|
getNote(id: string): Promise<Note | null>;
|
|
115
156
|
getNoteByPath(path: string): Promise<Note | null>;
|
|
116
157
|
getNotes(ids: string[]): Promise<Note[]>;
|
|
117
|
-
updateNote(id: string, updates: { content?: string; path?: string; metadata?: Record<string, unknown>; created_at?: string; skipUpdatedAt?: boolean; if_updated_at?: string }): Promise<Note>;
|
|
158
|
+
updateNote(id: string, updates: { content?: string; append?: string; prepend?: string; path?: string; metadata?: Record<string, unknown>; created_at?: string; skipUpdatedAt?: boolean; if_updated_at?: string }): Promise<Note>;
|
|
118
159
|
deleteNote(id: string): Promise<void>;
|
|
119
160
|
queryNotes(opts: QueryOpts): Promise<Note[]>;
|
|
120
161
|
searchNotes(query: string, opts?: { tags?: string[]; limit?: number }): Promise<Note[]>;
|
|
@@ -122,6 +163,13 @@ export interface Store {
|
|
|
122
163
|
// Tags
|
|
123
164
|
tagNote(noteId: string, tags: string[]): Promise<void>;
|
|
124
165
|
untagNote(noteId: string, tags: string[]): Promise<void>;
|
|
166
|
+
/**
|
|
167
|
+
* Expand a set of tag names to the union of `{tag} ∪ descendants(tag)` for
|
|
168
|
+
* each input, using the `_tags/<name>` config-note hierarchy. Always
|
|
169
|
+
* includes each input tag in the result. Used by tag-scoped tokens to
|
|
170
|
+
* compute the effective allowlisted tag-set at auth time.
|
|
171
|
+
*/
|
|
172
|
+
expandTagsWithDescendants(tags: string[]): Promise<Set<string>>;
|
|
125
173
|
listTags(): Promise<{ name: string; count: number }[]>;
|
|
126
174
|
deleteTag(name: string): Promise<{ deleted: boolean; notes_untagged: number }>;
|
|
127
175
|
renameTag(
|
|
@@ -151,12 +199,58 @@ export interface Store {
|
|
|
151
199
|
traverseLinks(noteId: string, opts?: { max_depth?: number; relationship?: string }): Promise<{ noteId: string; depth: number; relationship: string; direction: "outbound" | "inbound" }[]>;
|
|
152
200
|
findPath(sourceId: string, targetId: string, opts?: { max_depth?: number }): Promise<{ path: string[]; relationships: string[] } | null>;
|
|
153
201
|
|
|
154
|
-
// Tag schemas
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
202
|
+
// Tag schemas — schema-only facade (description + fields). Back-compat
|
|
203
|
+
// surface for v13-and-earlier callers; reads/writes route through the
|
|
204
|
+
// post-v14 `tags` row directly.
|
|
205
|
+
listTagSchemas(): Promise<{ tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[]; indexed?: boolean }> }[]>;
|
|
206
|
+
getTagSchema(tag: string): Promise<{ tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[]; indexed?: boolean }> } | null>;
|
|
207
|
+
upsertTagSchema(tag: string, schema: { description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[]; indexed?: boolean }> }): Promise<{ tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[]; indexed?: boolean }> }>;
|
|
158
208
|
deleteTagSchema(tag: string): Promise<boolean>;
|
|
159
|
-
getTagSchemaMap(): Promise<Record<string, { description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> }>>;
|
|
209
|
+
getTagSchemaMap(): Promise<Record<string, { description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[]; indexed?: boolean }> }>>;
|
|
210
|
+
|
|
211
|
+
// Tag records — full v14 identity row (description + fields + typed
|
|
212
|
+
// relationships + parent_names + timestamps). See
|
|
213
|
+
// parachute-patterns/patterns/tag-data-model.md.
|
|
214
|
+
listTagRecords(): Promise<TagRecord[]>;
|
|
215
|
+
getTagRecord(tag: string): Promise<TagRecord | null>;
|
|
216
|
+
/**
|
|
217
|
+
* Partial upsert. Any patch field left undefined is preserved; pass
|
|
218
|
+
* null to clear. Touching `parent_names` invalidates the tag-hierarchy
|
|
219
|
+
* cache. Returns the post-write row.
|
|
220
|
+
*/
|
|
221
|
+
upsertTagRecord(
|
|
222
|
+
tag: string,
|
|
223
|
+
patch: {
|
|
224
|
+
description?: string | null;
|
|
225
|
+
fields?: Record<string, TagFieldSchema> | null;
|
|
226
|
+
relationships?: Record<string, TagRelationship> | null;
|
|
227
|
+
parent_names?: string[] | null;
|
|
228
|
+
},
|
|
229
|
+
): Promise<TagRecord>;
|
|
230
|
+
|
|
231
|
+
// Schema validation (post-v15: backed by `note_schemas` + `schema_mappings`
|
|
232
|
+
// tables). Returns null when no schema applies to the given note. The
|
|
233
|
+
// underlying resolver is in-memory after the first lazy load.
|
|
234
|
+
validateNoteAgainstSchemas(note: { path?: string | null; tags?: string[]; metadata?: Record<string, unknown> }): {
|
|
235
|
+
schemas: string[];
|
|
236
|
+
warnings: { field: string; schema: string; reason: "missing_required" | "type_mismatch" | "enum_mismatch"; message: string }[];
|
|
237
|
+
} | null;
|
|
238
|
+
|
|
239
|
+
// Note schemas (post-v15 — the writable surface that drives validation).
|
|
240
|
+
listNoteSchemas(): Promise<NoteSchemaRecord[]>;
|
|
241
|
+
getNoteSchema(name: string): Promise<NoteSchemaRecord | null>;
|
|
242
|
+
/**
|
|
243
|
+
* Partial-upsert. Auto-creates the row if missing. Any patch field left
|
|
244
|
+
* undefined is preserved; pass null to clear. Empty `required: []`
|
|
245
|
+
* collapses to null.
|
|
246
|
+
*/
|
|
247
|
+
upsertNoteSchema(name: string, patch: NoteSchemaPatch): Promise<NoteSchemaRecord>;
|
|
248
|
+
deleteNoteSchema(name: string): Promise<boolean>;
|
|
249
|
+
|
|
250
|
+
// Schema mappings (post-v15 — replaces the singleton `_schema_defaults`).
|
|
251
|
+
listSchemaMappings(opts?: ListMappingsOpts): Promise<SchemaMappingRecord[]>;
|
|
252
|
+
setSchemaMapping(schema_name: string, match_kind: SchemaMappingKind, match_value: string): Promise<void>;
|
|
253
|
+
deleteSchemaMapping(schema_name: string, match_kind: SchemaMappingKind, match_value: string): Promise<boolean>;
|
|
160
254
|
|
|
161
255
|
// Attachments
|
|
162
256
|
addAttachment(noteId: string, path: string, mimeType: string, metadata?: Record<string, unknown>): Promise<Attachment>;
|
package/core/src/wikilinks.ts
CHANGED
|
@@ -44,7 +44,7 @@ export function parseWikilinks(content: string): ParsedWikilink[] {
|
|
|
44
44
|
|
|
45
45
|
while ((match = regex.exec(stripped)) !== null) {
|
|
46
46
|
const embed = match[1] === "!";
|
|
47
|
-
const inner = match[2]
|
|
47
|
+
const inner = match[2]!;
|
|
48
48
|
|
|
49
49
|
// Split on | for display text: [[target|display]]
|
|
50
50
|
const pipeIdx = inner.indexOf("|");
|
|
@@ -133,7 +133,7 @@ export function resolveWikilink(db: Database, target: string): string | null {
|
|
|
133
133
|
)
|
|
134
134
|
`).all(target, `%/${target}`) as { id: string }[];
|
|
135
135
|
|
|
136
|
-
if (basename.length === 1) return basename[0]
|
|
136
|
+
if (basename.length === 1) return basename[0]!.id;
|
|
137
137
|
|
|
138
138
|
// Ambiguous or no match
|
|
139
139
|
return null;
|
|
@@ -171,7 +171,7 @@ export function resolveWikilinkDetailed(db: Database, target: string): WikilinkR
|
|
|
171
171
|
`).all(target, `%/${target}`) as { id: string; path: string }[];
|
|
172
172
|
|
|
173
173
|
if (basename.length === 1) {
|
|
174
|
-
return { resolved: true, note_id: basename[0]
|
|
174
|
+
return { resolved: true, note_id: basename[0]!.id, path: basename[0]!.path, candidates: [] };
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
if (basename.length > 1) {
|
package/package.json
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openparachute/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
|
|
5
5
|
"module": "src/cli.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"parachute-vault": "src/cli.ts"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"core/src",
|
|
13
|
+
"core/package.json",
|
|
14
|
+
".parachute",
|
|
15
|
+
"tsconfig.json"
|
|
16
|
+
],
|
|
10
17
|
"scripts": {
|
|
11
18
|
"start": "bun src/server.ts",
|
|
12
19
|
"cli": "bun src/cli.ts",
|
|
13
|
-
"test": "bun test src/",
|
|
14
|
-
"test:core": "cd core && node --experimental-vm-modules node_modules/vitest/dist/cli.js run"
|
|
20
|
+
"test": "bun test ./src/",
|
|
21
|
+
"test:core": "cd core && node --experimental-vm-modules node_modules/vitest/dist/cli.js run",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
15
23
|
},
|
|
16
24
|
"dependencies": {
|
|
17
25
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
26
|
+
"@openparachute/scope-guard": "^0.1.0",
|
|
27
|
+
"jose": "^6.2.2",
|
|
18
28
|
"otpauth": "^9.5.0",
|
|
19
29
|
"qrcode-terminal": "^0.12.0"
|
|
20
30
|
},
|