@openparachute/vault 0.4.0 → 0.4.4-rc.11
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 +191 -2
- package/core/src/core.test.ts +1295 -526
- package/core/src/mcp.ts +129 -428
- package/core/src/notes.ts +405 -32
- package/core/src/obsidian.ts +55 -177
- package/core/src/portable-md.test.ts +1001 -0
- package/core/src/portable-md.ts +1409 -0
- package/core/src/schema-defaults.ts +233 -171
- package/core/src/schema.ts +104 -32
- package/core/src/store.ts +103 -78
- package/core/src/tag-hierarchy.ts +36 -2
- package/core/src/types.ts +52 -42
- package/core/src/vault-projection.ts +309 -0
- package/package.json +2 -2
- package/src/auth-hub-jwt.test.ts +142 -13
- package/src/auth.ts +29 -0
- package/src/cli.ts +699 -141
- package/src/doctor.test.ts +7 -6
- package/src/hub-jwt.test.ts +16 -5
- package/src/hub-jwt.ts +9 -0
- package/src/mcp-http.ts +4 -2
- package/src/mcp-install-interactive.test.ts +883 -0
- package/src/mcp-install-interactive.ts +412 -0
- package/src/mcp-install.test.ts +957 -5
- package/src/mcp-install.ts +580 -13
- package/src/mcp-tools.ts +101 -90
- package/src/routes.ts +330 -207
- package/src/routing.test.ts +12 -12
- package/src/routing.ts +0 -2
- package/src/tokens-routes.test.ts +11 -4
- package/src/vault.test.ts +1052 -333
- package/core/src/note-schemas.ts +0 -232
package/core/src/types.ts
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
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
2
|
|
|
11
3
|
// ---- Re-exports ----
|
|
12
4
|
|
|
13
5
|
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
6
|
|
|
22
7
|
// ---- Note ----
|
|
23
8
|
|
|
@@ -95,12 +80,14 @@ export interface QueryOpts {
|
|
|
95
80
|
// as the common path; specifying both this and `dateFilter` rejects.
|
|
96
81
|
dateFrom?: string; // ISO date
|
|
97
82
|
dateTo?: string; // ISO date
|
|
98
|
-
// Generalized date range. `field` defaults to `created_at`;
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
83
|
+
// Generalized date range. `field` defaults to `created_at`; `updated_at`
|
|
84
|
+
// is also a recognized real column (the incremental-rebuild path —
|
|
85
|
+
// vault#285 1.5). Any other field must be declared `indexed: true` in a
|
|
86
|
+
// tag schema (so the SQL hits a real B-tree index, same contract as
|
|
87
|
+
// `metadata` operator queries and `orderBy`). Use this to filter on a
|
|
88
|
+
// *content* date — an email's received date, a meeting's scheduled
|
|
89
|
+
// date — rather than the ingestion timestamp, or on `updated_at` to ask
|
|
90
|
+
// "what changed since X."
|
|
104
91
|
dateFilter?: {
|
|
105
92
|
field?: string;
|
|
106
93
|
from?: string;
|
|
@@ -156,6 +143,21 @@ export interface Store {
|
|
|
156
143
|
getNoteByPath(path: string): Promise<Note | null>;
|
|
157
144
|
getNotes(ids: string[]): Promise<Note[]>;
|
|
158
145
|
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>;
|
|
146
|
+
/**
|
|
147
|
+
* Set a note's `created_at` and `updated_at` explicitly. Import-only:
|
|
148
|
+
* used by the portable-md round-trip path to restore timestamps from
|
|
149
|
+
* the export bytes. The regular `updateNote` either bumps `updated_at`
|
|
150
|
+
* to wall-clock-now or (with `skipUpdatedAt: true`) leaves it
|
|
151
|
+
* untouched — neither shape lets the importer write a specific
|
|
152
|
+
* historical timestamp. Bypasses hooks. See vault#308 PR 2.
|
|
153
|
+
*/
|
|
154
|
+
restoreNoteTimestamps(id: string, createdAt: string, updatedAt: string): Promise<void>;
|
|
155
|
+
/**
|
|
156
|
+
* Sync wikilinks for every note in the vault. Cheap O(n) walk; used
|
|
157
|
+
* after bulk-imports to rebuild link rows from `[[brackets]]` in
|
|
158
|
+
* content. Returns counts for caller logging.
|
|
159
|
+
*/
|
|
160
|
+
syncAllWikilinks(): Promise<{ synced: number; totalAdded: number; totalRemoved: number }>;
|
|
159
161
|
deleteNote(id: string): Promise<void>;
|
|
160
162
|
queryNotes(opts: QueryOpts): Promise<Note[]>;
|
|
161
163
|
searchNotes(query: string, opts?: { tags?: string[]; limit?: number }): Promise<Note[]>;
|
|
@@ -175,7 +177,19 @@ export interface Store {
|
|
|
175
177
|
renameTag(
|
|
176
178
|
oldName: string,
|
|
177
179
|
newName: string,
|
|
178
|
-
): Promise<
|
|
180
|
+
): Promise<
|
|
181
|
+
| {
|
|
182
|
+
renamed: number;
|
|
183
|
+
sub_tags_renamed: number;
|
|
184
|
+
parent_refs_updated: number;
|
|
185
|
+
tokens_updated: number;
|
|
186
|
+
indexed_field_declarers_updated: number;
|
|
187
|
+
notes_rewritten: number;
|
|
188
|
+
paths_renamed: number;
|
|
189
|
+
}
|
|
190
|
+
| { error: "not_found" }
|
|
191
|
+
| { error: "target_exists"; conflicting: string[] }
|
|
192
|
+
>;
|
|
179
193
|
mergeTags(
|
|
180
194
|
sources: string[],
|
|
181
195
|
target: string,
|
|
@@ -228,30 +242,26 @@ export interface Store {
|
|
|
228
242
|
},
|
|
229
243
|
): Promise<TagRecord>;
|
|
230
244
|
|
|
231
|
-
// Schema validation (post-
|
|
232
|
-
//
|
|
233
|
-
//
|
|
245
|
+
// Schema validation (post-v17: backed by `tags.fields` only — the
|
|
246
|
+
// standalone note_schemas + schema_mappings subsystem retired in v17, see
|
|
247
|
+
// vault#267). Post vault#270 the resolver walks `parent_names` so a note's
|
|
248
|
+
// effective fields include all ancestors' declarations (first-in-walk wins
|
|
249
|
+
// on conflict, surfaced as `schema_conflict` warnings); a tag named
|
|
250
|
+
// `_default` is the implicit universal parent. Returns null when no
|
|
251
|
+
// ancestor declares any fields. The underlying resolver is in-memory after
|
|
252
|
+
// the first lazy load.
|
|
234
253
|
validateNoteAgainstSchemas(note: { path?: string | null; tags?: string[]; metadata?: Record<string, unknown> }): {
|
|
235
254
|
schemas: string[];
|
|
236
|
-
warnings: {
|
|
255
|
+
warnings: {
|
|
256
|
+
field: string;
|
|
257
|
+
schema: string;
|
|
258
|
+
reason: "type_mismatch" | "enum_mismatch" | "schema_conflict";
|
|
259
|
+
message: string;
|
|
260
|
+
/** Set only on `schema_conflict` — the tag whose declaration was overridden. */
|
|
261
|
+
loser_schema?: string;
|
|
262
|
+
}[];
|
|
237
263
|
} | null;
|
|
238
264
|
|
|
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>;
|
|
254
|
-
|
|
255
265
|
// Attachments
|
|
256
266
|
addAttachment(noteId: string, path: string, mimeType: string, metadata?: Record<string, unknown>): Promise<Attachment>;
|
|
257
267
|
getAttachments(noteId: string): Promise<Attachment[]>;
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault projection — computes a comprehensive description of the vault
|
|
3
|
+
* (tags-with-schemas + effective inheritance + indexed-field catalog +
|
|
4
|
+
* query hints) shared by two consumers:
|
|
5
|
+
*
|
|
6
|
+
* - `vault-info` MCP tool — returns the full projection as a JSON object
|
|
7
|
+
* so an agent can request a refresh mid-session.
|
|
8
|
+
* - `getServerInstruction` (MCP `initialize` response) — renders the
|
|
9
|
+
* same projection as a terse markdown brief sent once at connect.
|
|
10
|
+
*
|
|
11
|
+
* The projection lives outside the per-note path: it describes what kinds
|
|
12
|
+
* of notes and queries are available, not the contents. Nothing here
|
|
13
|
+
* depends on auth/scopes — both consumers compose this with vault config
|
|
14
|
+
* (name/description) and any policy-driven framing on top.
|
|
15
|
+
*
|
|
16
|
+
* See vault#271 for design notes.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Database } from "bun:sqlite";
|
|
20
|
+
import {
|
|
21
|
+
loadSchemaConfig,
|
|
22
|
+
resolveNoteSchemas,
|
|
23
|
+
walkAncestors,
|
|
24
|
+
type ResolvedSchemas,
|
|
25
|
+
type SchemaField,
|
|
26
|
+
} from "./schema-defaults.ts";
|
|
27
|
+
import { listIndexedFields } from "./indexed-fields.ts";
|
|
28
|
+
import {
|
|
29
|
+
listTagRecords,
|
|
30
|
+
type TagFieldSchema,
|
|
31
|
+
type TagRecord,
|
|
32
|
+
} from "./tag-schemas.ts";
|
|
33
|
+
import { DEFAULT_TAG_NAME } from "./tag-hierarchy.ts";
|
|
34
|
+
import * as noteOps from "./notes.ts";
|
|
35
|
+
import type { VaultStats } from "./types.ts";
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Types
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export interface ProjectionTag {
|
|
42
|
+
name: string;
|
|
43
|
+
description: string | null;
|
|
44
|
+
/** Direct parents declared in `tags.parent_names` (verbatim, no walk). */
|
|
45
|
+
parents: string[];
|
|
46
|
+
/**
|
|
47
|
+
* Walk-order ancestor closure (parents → grandparents → …) including the
|
|
48
|
+
* implicit `_default` universal parent when present, with the tag itself
|
|
49
|
+
* excluded. Empty when the tag has no parents and no `_default` exists.
|
|
50
|
+
*/
|
|
51
|
+
effective_parents: string[];
|
|
52
|
+
/**
|
|
53
|
+
* Own field declarations (verbatim from `tags.fields`). Carries the full
|
|
54
|
+
* `TagFieldSchema` shape — `type` (string), optional `description`,
|
|
55
|
+
* `enum`, `indexed`. Width matches the on-disk row.
|
|
56
|
+
*/
|
|
57
|
+
fields: Record<string, TagFieldSchema> | null;
|
|
58
|
+
/**
|
|
59
|
+
* Merged field map = own ∪ inherited (first-in-walk wins, matching
|
|
60
|
+
* `resolveNoteSchemas`). Uses the `SchemaField` view returned by the
|
|
61
|
+
* resolver (narrower `type` enum). Empty when no ancestor — including
|
|
62
|
+
* the tag itself — declared anything.
|
|
63
|
+
*/
|
|
64
|
+
effective_fields: Record<string, SchemaField>;
|
|
65
|
+
relationships: TagRecord["relationships"] | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ProjectionIndexedField {
|
|
69
|
+
name: string;
|
|
70
|
+
/** User-facing field type ("string" | "integer" | "boolean") drawn from the first declarer. */
|
|
71
|
+
type: string;
|
|
72
|
+
tags: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface VaultProjection {
|
|
76
|
+
tags: ProjectionTag[];
|
|
77
|
+
indexed_fields: ProjectionIndexedField[];
|
|
78
|
+
query_hints: string[];
|
|
79
|
+
/** Included when the caller requests stats; omitted otherwise. */
|
|
80
|
+
stats?: VaultStats;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Inheritance helpers (built on the #272 resolver)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a single tag's effective inheritance.
|
|
89
|
+
*
|
|
90
|
+
* Built on top of `resolveNoteSchemas({ tags: [tag] })` so the walk order
|
|
91
|
+
* and conflict precedence match the runtime validator exactly. Returns:
|
|
92
|
+
*
|
|
93
|
+
* - `effective_parents`: walk-order ancestor list with the tag itself
|
|
94
|
+
* excluded. Includes `_default` when a `_default` row exists, regardless
|
|
95
|
+
* of whether the tag declares it (universal-parent semantics).
|
|
96
|
+
* - `effective_fields`: merged field map (first-in-walk wins). When no
|
|
97
|
+
* ancestor contributes, this equals the tag's own `fields`.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveTagInheritance(
|
|
100
|
+
resolved: ResolvedSchemas,
|
|
101
|
+
tag: string,
|
|
102
|
+
): { effective_parents: string[]; effective_fields: Record<string, SchemaField> } {
|
|
103
|
+
const resolution = resolveNoteSchemas(resolved, { tags: [tag] });
|
|
104
|
+
|
|
105
|
+
// resolveNoteSchemas returns effectiveTags (only fields-contributing tags).
|
|
106
|
+
// We need the full walk for effective_parents — replay using the same
|
|
107
|
+
// resolver helper so walk-order semantics stay in lockstep with #270.
|
|
108
|
+
const visited = new Set<string>();
|
|
109
|
+
const order: string[] = [];
|
|
110
|
+
walkAncestors(tag, resolved, visited, order);
|
|
111
|
+
if (resolved.allTags.has(DEFAULT_TAG_NAME) && !visited.has(DEFAULT_TAG_NAME)) {
|
|
112
|
+
walkAncestors(DEFAULT_TAG_NAME, resolved, visited, order);
|
|
113
|
+
}
|
|
114
|
+
const effective_parents = order.filter((t) => t !== tag);
|
|
115
|
+
|
|
116
|
+
const effective_fields: Record<string, SchemaField> = {};
|
|
117
|
+
for (const [field, { spec }] of resolution.mergedFields) {
|
|
118
|
+
effective_fields[field] = spec;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { effective_parents, effective_fields };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Projection
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Static query-hint catalog. Sent verbatim in both vault-info JSON and the
|
|
130
|
+
* connect-time markdown projection so an agent can self-orient without
|
|
131
|
+
* reading source. Edit here when query semantics change.
|
|
132
|
+
*/
|
|
133
|
+
export const QUERY_HINTS: readonly string[] = [
|
|
134
|
+
"query-notes { tag: \"X\" } — all notes with tag X (includes descendants per inheritance)",
|
|
135
|
+
"query-notes { tag: \"X\", metadata: { field: { op: value } } } — operator queries on indexed fields (eq/ne/gt/gte/lt/lte/in/not_in/exists)",
|
|
136
|
+
"query-notes { search: \"...\" } — full-text search across content",
|
|
137
|
+
"query-notes { near: { id: \"...\" }, depth: 2 } — graph neighborhood within N hops",
|
|
138
|
+
"query-notes { id: \"<note-id-or-path>\" } — fetch a single note by ID or path",
|
|
139
|
+
] as const;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the comprehensive vault projection. Pure read; no caches mutated.
|
|
143
|
+
*
|
|
144
|
+
* Shape rules:
|
|
145
|
+
*
|
|
146
|
+
* - `tags`: only tags carrying their own `description` or `fields`. A
|
|
147
|
+
* hierarchy-only tag (parent_names but no own schema) is omitted from
|
|
148
|
+
* the catalog — its semantics live in whichever ancestor contributes
|
|
149
|
+
* fields. `effective_fields` still surfaces the merged spec when the
|
|
150
|
+
* tag *does* appear (because it has its own description/fields).
|
|
151
|
+
*
|
|
152
|
+
* - `indexed_fields`: one entry per row in the `indexed_fields` table.
|
|
153
|
+
* The user-facing `type` is drawn from the first declarer's spec —
|
|
154
|
+
* declarers must agree on type (enforced at write time) so picking the
|
|
155
|
+
* first is unambiguous. `tags` lists every declarer, sorted.
|
|
156
|
+
*
|
|
157
|
+
* - `stats`: included when `opts.includeStats === true`. Uses the
|
|
158
|
+
* existing `getVaultStats` shape unchanged — camelCase keys, full
|
|
159
|
+
* monthly distribution.
|
|
160
|
+
*/
|
|
161
|
+
export function buildVaultProjection(
|
|
162
|
+
db: Database,
|
|
163
|
+
opts?: { includeStats?: boolean },
|
|
164
|
+
): VaultProjection {
|
|
165
|
+
const resolved = loadSchemaConfig(db);
|
|
166
|
+
const records = listTagRecords(db);
|
|
167
|
+
|
|
168
|
+
const tags: ProjectionTag[] = [];
|
|
169
|
+
for (const r of records) {
|
|
170
|
+
const hasOwnSchema =
|
|
171
|
+
(r.description !== undefined && r.description !== null) ||
|
|
172
|
+
(r.fields !== undefined && r.fields !== null && Object.keys(r.fields).length > 0);
|
|
173
|
+
if (!hasOwnSchema) continue;
|
|
174
|
+
|
|
175
|
+
const { effective_parents, effective_fields } = resolveTagInheritance(resolved, r.tag);
|
|
176
|
+
|
|
177
|
+
tags.push({
|
|
178
|
+
name: r.tag,
|
|
179
|
+
description: r.description ?? null,
|
|
180
|
+
parents: r.parent_names ?? [],
|
|
181
|
+
effective_parents,
|
|
182
|
+
fields: r.fields ?? null,
|
|
183
|
+
effective_fields,
|
|
184
|
+
relationships: r.relationships ?? null,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const indexedRows = listIndexedFields(db);
|
|
189
|
+
const indexed_fields: ProjectionIndexedField[] = indexedRows.map((row) => {
|
|
190
|
+
const declarers = [...row.declarerTags].sort();
|
|
191
|
+
// Recover the user-facing type from the first declarer's spec. Falls
|
|
192
|
+
// back to a sqlite-derived label if the declarer's row is gone (race;
|
|
193
|
+
// shouldn't happen because release drops the indexed_fields row, but
|
|
194
|
+
// robust against drift).
|
|
195
|
+
let userType = sqliteToUserType(row.sqliteType);
|
|
196
|
+
for (const t of declarers) {
|
|
197
|
+
const fields = resolved.tagToFields.get(t);
|
|
198
|
+
const declared = fields?.[row.field]?.type;
|
|
199
|
+
if (typeof declared === "string" && declared.length > 0) {
|
|
200
|
+
userType = declared;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { name: row.field, type: userType, tags: declarers };
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const projection: VaultProjection = {
|
|
208
|
+
tags,
|
|
209
|
+
indexed_fields,
|
|
210
|
+
query_hints: [...QUERY_HINTS],
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
if (opts?.includeStats) {
|
|
214
|
+
projection.stats = noteOps.getVaultStats(db);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return projection;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function sqliteToUserType(t: string): string {
|
|
221
|
+
if (t === "TEXT") return "string";
|
|
222
|
+
if (t === "INTEGER") return "integer";
|
|
223
|
+
return t.toLowerCase();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Markdown rendering — for getServerInstruction
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Render a vault projection as a terse markdown brief for the MCP
|
|
232
|
+
* `initialize` response. Keep dense — agents see this once at connect, and
|
|
233
|
+
* the goal is "everything an agent needs to start using the vault sensibly,
|
|
234
|
+
* with explicit pointers for refresh."
|
|
235
|
+
*
|
|
236
|
+
* Token budget guideline: ~600 tokens for a small vault (Aaron's, ~4 tags-
|
|
237
|
+
* with-schemas) and under ~5K tokens at 50 tags-with-schemas. Listing all
|
|
238
|
+
* tags-with-schemas inline is the default; cap heuristics can be added if
|
|
239
|
+
* a real test shape demands it.
|
|
240
|
+
*/
|
|
241
|
+
export function projectionToMarkdown(args: {
|
|
242
|
+
vaultName: string;
|
|
243
|
+
description?: string | null;
|
|
244
|
+
projection: VaultProjection;
|
|
245
|
+
}): string {
|
|
246
|
+
const { vaultName, description, projection } = args;
|
|
247
|
+
const stats = projection.stats;
|
|
248
|
+
|
|
249
|
+
const lines: string[] = [];
|
|
250
|
+
lines.push(`You are connected to Parachute Vault "${vaultName}".`);
|
|
251
|
+
if (description && description.trim().length > 0) {
|
|
252
|
+
lines.push("");
|
|
253
|
+
lines.push(description.trim());
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push("## Quick orientation (call `vault-info` for full schema)");
|
|
258
|
+
lines.push("");
|
|
259
|
+
|
|
260
|
+
if (stats) {
|
|
261
|
+
// Two distinct counts surface here so an agent doesn't conflate
|
|
262
|
+
// them (vault#274): `tagCount` is "tags ANY note uses" — driven by
|
|
263
|
+
// note_tags rows. `projection.tags.length` is "tags carrying a
|
|
264
|
+
// schema declaration" — strictly smaller and the relevant denominator
|
|
265
|
+
// for the schema-bearing list a few lines down. Showing only one
|
|
266
|
+
// hid the gap (e.g., 100 tags but only 5 with schemas read as
|
|
267
|
+
// "100 tags with schemas").
|
|
268
|
+
const noteCount = stats.totalNotes;
|
|
269
|
+
const tagCount = stats.tagCount;
|
|
270
|
+
const withSchemas = projection.tags.length;
|
|
271
|
+
const noteLabel = noteCount === 1 ? "note" : "notes";
|
|
272
|
+
const tagLabel = tagCount === 1 ? "tag" : "tags";
|
|
273
|
+
const tagSuffix = withSchemas > 0 ? `, ${withSchemas} with schemas` : "";
|
|
274
|
+
lines.push(`- ${noteCount} ${noteLabel}, ${tagCount} ${tagLabel} total${tagSuffix}`);
|
|
275
|
+
} else {
|
|
276
|
+
lines.push(`- (call \`vault-info { include_stats: true }\` for note/tag counts)`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (projection.tags.length === 0) {
|
|
280
|
+
lines.push(`- No tag schemas declared yet — every note is freeform.`);
|
|
281
|
+
} else {
|
|
282
|
+
const names = projection.tags.map((t) => t.name).join(", ");
|
|
283
|
+
lines.push(`- ${projection.tags.length} tag${projection.tags.length === 1 ? "" : "s"} with schemas: ${names}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (projection.indexed_fields.length > 0) {
|
|
287
|
+
lines.push(`- Indexed metadata fields (queryable with operators):`);
|
|
288
|
+
for (const f of projection.indexed_fields) {
|
|
289
|
+
const declarers = f.tags.map((t) => `#${t}`).join(", ");
|
|
290
|
+
lines.push(` - ${f.name} (${f.type}; from ${declarers})`);
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
lines.push(`- No indexed metadata fields.`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
lines.push("");
|
|
297
|
+
lines.push("## Querying");
|
|
298
|
+
lines.push("");
|
|
299
|
+
for (const hint of projection.query_hints) {
|
|
300
|
+
lines.push(`- \`${hint}\``);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
lines.push("");
|
|
304
|
+
lines.push("## Refreshing context");
|
|
305
|
+
lines.push("");
|
|
306
|
+
lines.push("If schema or tags change during this session, call `vault-info` to refresh the full projection. Call `list-tags { include_schema: true }` for tag-only details.");
|
|
307
|
+
|
|
308
|
+
return lines.join("\n");
|
|
309
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openparachute/vault",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4-rc.11",
|
|
4
4
|
"description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
|
|
5
5
|
"module": "src/cli.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
26
|
-
"@openparachute/scope-guard": "^0.
|
|
26
|
+
"@openparachute/scope-guard": "^0.2.0",
|
|
27
27
|
"jose": "^6.2.2",
|
|
28
28
|
"otpauth": "^9.5.0",
|
|
29
29
|
"qrcode-terminal": "^0.12.0"
|