@gmickel/gno 1.8.0 → 1.9.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 +6 -2
- package/assets/skill/SKILL.md +13 -0
- package/assets/skill/cli-reference.md +9 -0
- package/assets/skill/examples.md +21 -0
- package/assets/skill/mcp-reference.md +6 -0
- package/package.json +1 -1
- package/src/cli/commands/capture.ts +9 -6
- package/src/cli/commands/index-cmd.ts +17 -6
- package/src/cli/commands/shared.ts +17 -1
- package/src/cli/commands/update.ts +17 -6
- package/src/config/content-types.ts +140 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +14 -0
- package/src/config/loader.ts +11 -2
- package/src/config/types.ts +37 -1
- package/src/core/config-mutation.ts +14 -2
- package/src/core/note-presets.ts +61 -5
- package/src/ingestion/frontmatter.ts +77 -2
- package/src/ingestion/index.ts +2 -0
- package/src/ingestion/sync-options.ts +29 -0
- package/src/ingestion/sync.ts +104 -16
- package/src/ingestion/types.ts +14 -0
- package/src/mcp/tools/add-collection.ts +8 -5
- package/src/mcp/tools/capture.ts +5 -2
- package/src/mcp/tools/index-cmd.ts +13 -7
- package/src/mcp/tools/index.ts +8 -9
- package/src/mcp/tools/sync.ts +12 -6
- package/src/mcp/tools/workspace-write.ts +16 -10
- package/src/pipeline/hybrid.ts +2 -0
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +2 -0
- package/src/pipeline/vsearch.ts +4 -0
- package/src/sdk/client.ts +51 -24
- package/src/serve/background-runtime.ts +18 -4
- package/src/serve/config-sync.ts +9 -2
- package/src/serve/routes/api.ts +50 -24
- package/src/serve/watch-service.ts +12 -2
- package/src/store/migrations/009-content-type-rule-fingerprint.ts +36 -0
- package/src/store/migrations/index.ts +12 -1
- package/src/store/sqlite/adapter.ts +29 -14
- package/src/store/types.ts +6 -0
package/README.md
CHANGED
|
@@ -97,7 +97,11 @@ gno daemon --detach # headless continuous indexing (background; --status / --st
|
|
|
97
97
|
|
|
98
98
|
- **Second-brain capture**: `gno capture`, REST `/api/capture`, SDK
|
|
99
99
|
`client.capture()`, MCP `gno_capture`, and Web UI Quick Capture write
|
|
100
|
-
provenance-rich notes from text, stdin, or files
|
|
100
|
+
provenance-rich notes from text, stdin, or files, including typed presets for
|
|
101
|
+
ideas, people, company/projects, and meetings
|
|
102
|
+
- **Schema-lite content types**: optional `contentTypes` rules map configured
|
|
103
|
+
frontmatter `type` values or path prefixes to canonical `contentType` metadata
|
|
104
|
+
in JSON search/query results
|
|
101
105
|
- **Publish to [gno.sh](https://gno.sh/publish)**: new `gno publish export` CLI and Web UI action produce a self-contained artifact you upload to the hosted reader — public, secret, invite-only, or locally encrypted before upload
|
|
102
106
|
- **Retrieval Quality Upgrade**: stronger BM25 lexical handling, code-aware chunking, terminal result hyperlinks, and per-collection model overrides
|
|
103
107
|
- **Code Embedding Benchmarks**: new benchmark workflow across canonical, real-GNO, and pinned OSS slices for comparing alternate embedding models
|
|
@@ -546,7 +550,7 @@ Open `http://localhost:3000` to:
|
|
|
546
550
|
- **Browse**: Cross-collection tree workspace with folder detail panes and per-tab browse context
|
|
547
551
|
- **Edit**: Create, edit, and delete documents with live preview
|
|
548
552
|
- **Create in place**: New notes in the current folder/collection with presets and command-palette flows
|
|
549
|
-
- **Capture with provenance**: `gno capture` and Web UI Quick Capture write quick notes to an editable collection with structured `source:` metadata and a receipt that separates write, sync, and embed state
|
|
553
|
+
- **Capture with provenance**: `gno capture` and Web UI Quick Capture write quick notes to an editable collection with structured `source:` metadata, typed preset scaffolds, and a receipt that separates write, sync, and embed state
|
|
550
554
|
- **Same capture contract everywhere**: CLI, MCP `gno_capture`, REST `/api/capture`, SDK `client.capture()`, and Web UI Quick Capture return the same provenance receipt shape
|
|
551
555
|
- **Ask**: AI-powered Q&A with citations
|
|
552
556
|
- **Manage Collections**: Add, remove, and re-index collections
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -214,8 +214,21 @@ Use `gno capture` for quick second-brain writes into an editable collection:
|
|
|
214
214
|
```bash
|
|
215
215
|
gno capture "thought to remember"
|
|
216
216
|
gno capture --file ./clip.md --source-url https://example.com --source-kind web --json
|
|
217
|
+
gno capture --preset person --title "Jane Doe" --folder people/
|
|
218
|
+
gno capture --preset meeting --title "Weekly sync" --folder meetings/
|
|
217
219
|
```
|
|
218
220
|
|
|
221
|
+
Preset IDs: `blank`, `project-note`, `research-note`, `decision-note`,
|
|
222
|
+
`prompt-pattern`, `source-summary`, `idea-original`, `person`,
|
|
223
|
+
`company-project`, `meeting`.
|
|
224
|
+
|
|
225
|
+
For second-brain pages, prefer the typed presets:
|
|
226
|
+
|
|
227
|
+
- `idea-original`: exact idea phrasing, context, related concepts, publish potential.
|
|
228
|
+
- `person`: current state, relationship, assessment, open threads, timeline.
|
|
229
|
+
- `company-project`: state, changes, decisions, people, timeline.
|
|
230
|
+
- `meeting`: synthesis/action analysis above `## Timeline`; raw notes below.
|
|
231
|
+
|
|
219
232
|
The JSON receipt reports write, sync, and embed status separately. Generated
|
|
220
233
|
captures land under `inbox/YYYY-MM-DD/capture-<body-hash>.md` unless `--path`,
|
|
221
234
|
`--folder`, or `--title` overrides the path. Capture does not imply embedding
|
|
@@ -124,6 +124,15 @@ gno capture --file ./clip.md --source-url https://example.com --source-kind web
|
|
|
124
124
|
gno capture "meeting note" --quiet
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
Preset IDs: `blank`, `project-note`, `research-note`, `decision-note`,
|
|
128
|
+
`prompt-pattern`, `source-summary`, `idea-original`, `person`,
|
|
129
|
+
`company-project`, `meeting`.
|
|
130
|
+
|
|
131
|
+
Second-brain presets keep current synthesis above `## Timeline` and dated
|
|
132
|
+
evidence below it. Use `idea-original` for exact idea wording, `person` for
|
|
133
|
+
relationship/current-state notes, `company-project` for organizations or active
|
|
134
|
+
workstreams, and `meeting` for analysis above transcript/raw notes/action items.
|
|
135
|
+
|
|
127
136
|
Important behavior:
|
|
128
137
|
|
|
129
138
|
- Inline content, `--stdin`, and `--file` are mutually exclusive.
|
package/assets/skill/examples.md
CHANGED
|
@@ -359,6 +359,27 @@ gno capture "thought to remember"
|
|
|
359
359
|
gno capture --file ./clip.md --source-url https://example.com --source-kind web --json
|
|
360
360
|
```
|
|
361
361
|
|
|
362
|
+
### Typed second-brain presets
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
# Original idea: preserve exact phrasing and related concepts
|
|
366
|
+
gno capture --preset idea-original --title "Local-first inbox triage" --folder ideas/
|
|
367
|
+
|
|
368
|
+
# Person page: current synthesis plus relationship/open threads/timeline
|
|
369
|
+
gno capture --preset person --title "Jane Doe" --folder people/
|
|
370
|
+
|
|
371
|
+
# Company/project page: state, decisions, people, and timeline
|
|
372
|
+
gno capture --preset company-project --title "Acme renewal" --folder projects/
|
|
373
|
+
|
|
374
|
+
# Meeting page: analysis above raw notes, transcript, and action items
|
|
375
|
+
gno capture --preset meeting --title "Weekly sync" --folder meetings/
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
For these presets, keep current synthesis above `## Timeline`; put dated evidence
|
|
379
|
+
and raw chronology below it. If `contentTypes` are configured in `index.yml`,
|
|
380
|
+
matching frontmatter `type` or path prefixes become `contentType` in JSON search
|
|
381
|
+
results.
|
|
382
|
+
|
|
362
383
|
### Web UI quick capture
|
|
363
384
|
|
|
364
385
|
Press **N** in `gno serve`, write the note, and open **Source** only when you
|
|
@@ -76,6 +76,12 @@ frontmatter and returns the same provenance receipt shape as CLI, REST, and SDK
|
|
|
76
76
|
capture, plus legacy MCP fields (`docid`, `absPath`, `overwritten`,
|
|
77
77
|
`serverInstanceId`).
|
|
78
78
|
|
|
79
|
+
`presetId` accepts `blank`, `project-note`, `research-note`, `decision-note`,
|
|
80
|
+
`prompt-pattern`, `source-summary`, `idea-original`, `person`,
|
|
81
|
+
`company-project`, or `meeting`. The typed second-brain presets use flat
|
|
82
|
+
frontmatter (`type`, `category`, `tags`) and a synthesis/timeline page pattern;
|
|
83
|
+
provenance still comes from the capture `source` fields, not the preset.
|
|
84
|
+
|
|
79
85
|
Use `collisionPolicy: "open_existing"` to return an existing note without
|
|
80
86
|
rewriting, `create_with_suffix` to create the next available path, or legacy
|
|
81
87
|
`overwrite: true` to replace the target path. Capture content must be text, and
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from "../../core/capture";
|
|
27
27
|
import { writeCapturePlanFile } from "../../core/capture-write";
|
|
28
28
|
import { withWriteLock } from "../../core/file-lock";
|
|
29
|
-
import { defaultSyncService } from "../../ingestion";
|
|
29
|
+
import { defaultSyncService, withContentTypeRules } from "../../ingestion";
|
|
30
30
|
import { CliError } from "../errors";
|
|
31
31
|
import { initStore } from "./shared";
|
|
32
32
|
|
|
@@ -140,7 +140,7 @@ export async function capture(
|
|
|
140
140
|
throw new CliError("VALIDATION", storeInit.error);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
const { store, collections } = storeInit;
|
|
143
|
+
const { store, collections, config } = storeInit;
|
|
144
144
|
try {
|
|
145
145
|
const collectionName = options.collection?.trim();
|
|
146
146
|
const collection = collectionName
|
|
@@ -222,10 +222,13 @@ export async function capture(
|
|
|
222
222
|
collection,
|
|
223
223
|
store,
|
|
224
224
|
[plan.relPath],
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
withContentTypeRules(
|
|
226
|
+
{
|
|
227
|
+
runUpdateCmd: false,
|
|
228
|
+
gitPull: false,
|
|
229
|
+
},
|
|
230
|
+
config
|
|
231
|
+
)
|
|
229
232
|
);
|
|
230
233
|
const syncResult = syncResults[0];
|
|
231
234
|
const docResult = await store.getDocument(collection.name, plan.relPath);
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
* @module src/cli/commands/indexCmd
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
defaultSyncService,
|
|
10
|
+
type SyncResult,
|
|
11
|
+
withContentTypeRules,
|
|
12
|
+
} from "../../ingestion";
|
|
9
13
|
import { formatSyncResultLines, initStore } from "./shared";
|
|
10
14
|
|
|
11
15
|
/**
|
|
@@ -55,14 +59,21 @@ export async function index(options: IndexOptions = {}): Promise<IndexResult> {
|
|
|
55
59
|
return { success: false, error: initResult.error };
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
const { store, collections } = initResult;
|
|
62
|
+
const { store, collections, config } = initResult;
|
|
59
63
|
|
|
60
64
|
try {
|
|
61
65
|
// Run sync service (update phase)
|
|
62
|
-
const syncResult = await defaultSyncService.syncAll(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
const syncResult = await defaultSyncService.syncAll(
|
|
67
|
+
collections,
|
|
68
|
+
store,
|
|
69
|
+
withContentTypeRules(
|
|
70
|
+
{
|
|
71
|
+
gitPull: options.gitPull,
|
|
72
|
+
runUpdateCmd: true,
|
|
73
|
+
},
|
|
74
|
+
config
|
|
75
|
+
)
|
|
76
|
+
);
|
|
66
77
|
|
|
67
78
|
// Embedding phase
|
|
68
79
|
const embedSkipped = options.noEmbed ?? false;
|
|
@@ -10,7 +10,12 @@ import type { SyncResult } from "../../ingestion";
|
|
|
10
10
|
import type { SearchResults } from "../../pipeline/types";
|
|
11
11
|
|
|
12
12
|
import { decorateUriForIndex, getIndexDbPath } from "../../app/constants";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
getConfigPaths,
|
|
15
|
+
isInitialized,
|
|
16
|
+
loadConfig,
|
|
17
|
+
writeConfigWarningsToStderr,
|
|
18
|
+
} from "../../config";
|
|
14
19
|
import { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
15
20
|
|
|
16
21
|
/**
|
|
@@ -61,6 +66,7 @@ export async function initStore(
|
|
|
61
66
|
if (!configResult.ok) {
|
|
62
67
|
return { ok: false, error: configResult.error.message };
|
|
63
68
|
}
|
|
69
|
+
writeConfigWarningsToStderr(configResult.warnings);
|
|
64
70
|
const config = configResult.value;
|
|
65
71
|
|
|
66
72
|
// Filter to single collection if specified
|
|
@@ -165,6 +171,16 @@ export function formatSyncResultLines(
|
|
|
165
171
|
lines.push(` [${err.code}] ${err.relPath}: ${err.message}`);
|
|
166
172
|
}
|
|
167
173
|
}
|
|
174
|
+
if (options.verbose && c.files?.length) {
|
|
175
|
+
for (const file of c.files) {
|
|
176
|
+
if (!file.contentType || !file.contentTypeSource) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
lines.push(
|
|
180
|
+
` [${file.status}] ${file.relPath}: contentType=${file.contentType} (${file.contentTypeSource})`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
168
184
|
}
|
|
169
185
|
|
|
170
186
|
lines.push("");
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
* @module src/cli/commands/update
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
defaultSyncService,
|
|
10
|
+
type SyncResult,
|
|
11
|
+
withContentTypeRules,
|
|
12
|
+
} from "../../ingestion";
|
|
9
13
|
import { formatSyncResultLines, initStore } from "./shared";
|
|
10
14
|
|
|
11
15
|
/**
|
|
@@ -43,14 +47,21 @@ export async function update(
|
|
|
43
47
|
return { success: false, error: initResult.error };
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
const { store, collections } = initResult;
|
|
50
|
+
const { store, collections, config } = initResult;
|
|
47
51
|
|
|
48
52
|
try {
|
|
49
53
|
// Run sync service
|
|
50
|
-
const result = await defaultSyncService.syncAll(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
const result = await defaultSyncService.syncAll(
|
|
55
|
+
collections,
|
|
56
|
+
store,
|
|
57
|
+
withContentTypeRules(
|
|
58
|
+
{
|
|
59
|
+
gitPull: options.gitPull,
|
|
60
|
+
runUpdateCmd: true,
|
|
61
|
+
},
|
|
62
|
+
config
|
|
63
|
+
)
|
|
64
|
+
);
|
|
54
65
|
|
|
55
66
|
return { success: true, result };
|
|
56
67
|
} finally {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content type config normalization.
|
|
3
|
+
*
|
|
4
|
+
* @module src/config/content-types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { NotePresetId } from "../core/note-presets";
|
|
8
|
+
import type { Config, ContentTypeConfig } from "./types";
|
|
9
|
+
|
|
10
|
+
import { NOTE_PRESETS } from "../core/note-presets";
|
|
11
|
+
|
|
12
|
+
export type ConfigWarningCode =
|
|
13
|
+
| "UNKNOWN_CONTENT_TYPE_PRESET"
|
|
14
|
+
| "DUPLICATE_CONTENT_TYPE_PREFIX";
|
|
15
|
+
|
|
16
|
+
export interface ConfigWarning {
|
|
17
|
+
code: ConfigWarningCode;
|
|
18
|
+
message: string;
|
|
19
|
+
path: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NormalizedContentTypeRule extends ContentTypeConfig {
|
|
23
|
+
preset: NotePresetId;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ContentTypeNormalizationResult {
|
|
27
|
+
rules: NormalizedContentTypeRule[];
|
|
28
|
+
warnings: ConfigWarning[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ConfigNormalizationResult {
|
|
32
|
+
config: Config;
|
|
33
|
+
warnings: ConfigWarning[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const NOTE_PRESET_IDS = new Set<NotePresetId>(
|
|
37
|
+
NOTE_PRESETS.map((preset) => preset.id)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function isNotePresetId(value: string): value is NotePresetId {
|
|
41
|
+
return NOTE_PRESET_IDS.has(value as NotePresetId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function normalizeContentTypes(
|
|
45
|
+
contentTypes: ContentTypeConfig[]
|
|
46
|
+
): ContentTypeNormalizationResult {
|
|
47
|
+
const warnings: ConfigWarning[] = [];
|
|
48
|
+
const seenPrefixes = new Set<string>();
|
|
49
|
+
const rules: NormalizedContentTypeRule[] = [];
|
|
50
|
+
|
|
51
|
+
for (const [typeIndex, contentType] of contentTypes.entries()) {
|
|
52
|
+
const path = `contentTypes[${typeIndex}]`;
|
|
53
|
+
if (!isNotePresetId(contentType.preset)) {
|
|
54
|
+
warnings.push({
|
|
55
|
+
code: "UNKNOWN_CONTENT_TYPE_PRESET",
|
|
56
|
+
path: `${path}.preset`,
|
|
57
|
+
message: `Dropped content type "${contentType.id}" because preset "${contentType.preset}" is not a known note preset.`,
|
|
58
|
+
});
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const prefixes: string[] = [];
|
|
63
|
+
for (const [prefixIndex, prefix] of contentType.prefixes.entries()) {
|
|
64
|
+
if (seenPrefixes.has(prefix)) {
|
|
65
|
+
warnings.push({
|
|
66
|
+
code: "DUPLICATE_CONTENT_TYPE_PREFIX",
|
|
67
|
+
path: `${path}.prefixes[${prefixIndex}]`,
|
|
68
|
+
message: `Dropped duplicate content type prefix "${prefix}" from "${contentType.id}".`,
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
seenPrefixes.add(prefix);
|
|
74
|
+
prefixes.push(prefix);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (prefixes.length === 0) {
|
|
78
|
+
rules.push({
|
|
79
|
+
...contentType,
|
|
80
|
+
preset: contentType.preset,
|
|
81
|
+
prefixes,
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
rules.push({
|
|
87
|
+
...contentType,
|
|
88
|
+
preset: contentType.preset,
|
|
89
|
+
prefixes: [...prefixes].sort((a, b) => b.length - a.length),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
rules.sort((a, b) => {
|
|
94
|
+
const aLongest = a.prefixes[0]?.length ?? 0;
|
|
95
|
+
const bLongest = b.prefixes[0]?.length ?? 0;
|
|
96
|
+
return bLongest - aLongest;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return { rules, warnings };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeConfigContentTypes(
|
|
103
|
+
config: Config
|
|
104
|
+
): ConfigNormalizationResult {
|
|
105
|
+
const normalized = normalizeContentTypes(config.contentTypes ?? []);
|
|
106
|
+
return {
|
|
107
|
+
config: {
|
|
108
|
+
...config,
|
|
109
|
+
contentTypes: normalized.rules,
|
|
110
|
+
},
|
|
111
|
+
warnings: normalized.warnings,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function fingerprintContentTypeRules(
|
|
116
|
+
rules: NormalizedContentTypeRule[]
|
|
117
|
+
): string {
|
|
118
|
+
const canonical = rules.map((rule) => ({
|
|
119
|
+
id: rule.id,
|
|
120
|
+
preset: rule.preset,
|
|
121
|
+
prefixes: rule.prefixes,
|
|
122
|
+
}));
|
|
123
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
124
|
+
hasher.update(JSON.stringify(canonical));
|
|
125
|
+
return hasher.digest("hex");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function formatConfigWarning(warning: ConfigWarning): string {
|
|
129
|
+
return `[config] ${warning.path}: ${warning.message}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function formatConfigWarnings(warnings?: ConfigWarning[]): string[] {
|
|
133
|
+
return (warnings ?? []).map(formatConfigWarning);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function writeConfigWarningsToStderr(warnings?: ConfigWarning[]): void {
|
|
137
|
+
for (const warning of formatConfigWarnings(warnings)) {
|
|
138
|
+
process.stderr.write(`${warning}\n`);
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/config/defaults.ts
CHANGED
package/src/config/index.ts
CHANGED
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export { createDefaultConfig } from "./defaults";
|
|
8
|
+
export {
|
|
9
|
+
type ConfigWarning,
|
|
10
|
+
fingerprintContentTypeRules,
|
|
11
|
+
formatConfigWarning,
|
|
12
|
+
formatConfigWarnings,
|
|
13
|
+
normalizeConfigContentTypes,
|
|
14
|
+
normalizeContentTypes,
|
|
15
|
+
type NormalizedContentTypeRule,
|
|
16
|
+
writeConfigWarningsToStderr,
|
|
17
|
+
} from "./content-types";
|
|
8
18
|
// Loading
|
|
9
19
|
export {
|
|
10
20
|
isInitialized,
|
|
@@ -40,6 +50,10 @@ export {
|
|
|
40
50
|
CollectionSchema,
|
|
41
51
|
type Config,
|
|
42
52
|
ConfigSchema,
|
|
53
|
+
CONTENT_TYPE_GRAPH_HINTS,
|
|
54
|
+
type ContentTypeConfig,
|
|
55
|
+
type ContentTypeGraphHint,
|
|
56
|
+
ContentTypeSchema,
|
|
43
57
|
type Context,
|
|
44
58
|
ContextSchema,
|
|
45
59
|
DEFAULT_EXCLUDES,
|
package/src/config/loader.ts
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ZodError } from "zod";
|
|
9
9
|
|
|
10
|
+
import {
|
|
11
|
+
normalizeConfigContentTypes,
|
|
12
|
+
type ConfigWarning,
|
|
13
|
+
} from "./content-types";
|
|
10
14
|
import { configExists, expandPath, getConfigPaths } from "./paths";
|
|
11
15
|
import { CONFIG_VERSION, type Config, ConfigSchema } from "./types";
|
|
12
16
|
|
|
@@ -15,7 +19,7 @@ import { CONFIG_VERSION, type Config, ConfigSchema } from "./types";
|
|
|
15
19
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
20
|
|
|
17
21
|
export type LoadResult<T> =
|
|
18
|
-
| { ok: true; value: T }
|
|
22
|
+
| { ok: true; value: T; warnings: ConfigWarning[] }
|
|
19
23
|
| { ok: false; error: LoadError };
|
|
20
24
|
|
|
21
25
|
export type LoadError =
|
|
@@ -128,7 +132,12 @@ export async function loadConfigFromPath(
|
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
|
|
135
|
+
const normalized = normalizeConfigContentTypes(result.data);
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
value: normalized.config,
|
|
139
|
+
warnings: normalized.warnings,
|
|
140
|
+
};
|
|
132
141
|
}
|
|
133
142
|
|
|
134
143
|
/**
|
package/src/config/types.ts
CHANGED
|
@@ -245,6 +245,37 @@ export const ModelConfigSchema = z.object({
|
|
|
245
245
|
|
|
246
246
|
export type ModelConfig = z.infer<typeof ModelConfigSchema>;
|
|
247
247
|
|
|
248
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
// Content Type Schema
|
|
250
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
export const CONTENT_TYPE_GRAPH_HINTS = [
|
|
253
|
+
"mentions",
|
|
254
|
+
"works_at",
|
|
255
|
+
"attended",
|
|
256
|
+
"decided",
|
|
257
|
+
"related_to",
|
|
258
|
+
] as const;
|
|
259
|
+
|
|
260
|
+
export type ContentTypeGraphHint = (typeof CONTENT_TYPE_GRAPH_HINTS)[number];
|
|
261
|
+
|
|
262
|
+
export const ContentTypeSchema = z.object({
|
|
263
|
+
/** Stable content type identifier */
|
|
264
|
+
id: z.string().min(1),
|
|
265
|
+
/** Relative path prefixes that map to this content type */
|
|
266
|
+
prefixes: z.array(z.string().min(1)),
|
|
267
|
+
/** Note preset ID, resolved post-parse so unknown refs warn-and-drop */
|
|
268
|
+
preset: z.string().min(1),
|
|
269
|
+
/** Reserved for fn-84 typed graph hints; accepted but no-op in fn-83 */
|
|
270
|
+
graphHints: z.array(z.string().min(1)).optional(),
|
|
271
|
+
/** Reserved for future ranking; accepted but no-op in fn-83 */
|
|
272
|
+
searchBoost: z.number().finite().optional(),
|
|
273
|
+
/** Marks time-oriented content types; accepted but no-op in fn-83 */
|
|
274
|
+
temporal: z.boolean().optional(),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
export type ContentTypeConfig = z.infer<typeof ContentTypeSchema>;
|
|
278
|
+
|
|
248
279
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
280
|
// Config Schema (root)
|
|
250
281
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -265,11 +296,16 @@ export const ConfigSchema = z.object({
|
|
|
265
296
|
/** Context metadata */
|
|
266
297
|
contexts: z.array(ContextSchema).default([]),
|
|
267
298
|
|
|
299
|
+
/** Opt-in schema-lite content type rules */
|
|
300
|
+
contentTypes: z.array(ContentTypeSchema).default([]),
|
|
301
|
+
|
|
268
302
|
/** Model configuration */
|
|
269
303
|
models: ModelConfigSchema.optional(),
|
|
270
304
|
});
|
|
271
305
|
|
|
272
|
-
export type Config = z.infer<typeof ConfigSchema
|
|
306
|
+
export type Config = Omit<z.infer<typeof ConfigSchema>, "contentTypes"> & {
|
|
307
|
+
contentTypes?: ContentTypeConfig[];
|
|
308
|
+
};
|
|
273
309
|
|
|
274
310
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
275
311
|
// Scope Utilities
|
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
import type { Config } from "../config/types";
|
|
8
8
|
import type { SqliteAdapter } from "../store/sqlite/adapter";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
formatConfigWarnings,
|
|
12
|
+
loadConfig,
|
|
13
|
+
normalizeConfigContentTypes,
|
|
14
|
+
saveConfig,
|
|
15
|
+
} from "../config";
|
|
11
16
|
|
|
12
17
|
export interface ConfigMutationContext {
|
|
13
18
|
store: SqliteAdapter;
|
|
@@ -53,6 +58,9 @@ export async function applyConfigChange<T = void>(
|
|
|
53
58
|
code: "LOAD_ERROR",
|
|
54
59
|
};
|
|
55
60
|
}
|
|
61
|
+
for (const warning of formatConfigWarnings(loadResult.warnings)) {
|
|
62
|
+
console.warn(warning);
|
|
63
|
+
}
|
|
56
64
|
|
|
57
65
|
const mutationResult = await mutate(loadResult.value);
|
|
58
66
|
if (!mutationResult.ok) {
|
|
@@ -63,7 +71,11 @@ export async function applyConfigChange<T = void>(
|
|
|
63
71
|
};
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
const
|
|
74
|
+
const normalized = normalizeConfigContentTypes(mutationResult.config);
|
|
75
|
+
for (const warning of formatConfigWarnings(normalized.warnings)) {
|
|
76
|
+
console.warn(warning);
|
|
77
|
+
}
|
|
78
|
+
const newConfig = normalized.config;
|
|
67
79
|
const saveResult = await saveConfig(newConfig, ctx.configPath);
|
|
68
80
|
if (!saveResult.ok) {
|
|
69
81
|
return {
|
package/src/core/note-presets.ts
CHANGED
|
@@ -14,7 +14,11 @@ export type NotePresetId =
|
|
|
14
14
|
| "research-note"
|
|
15
15
|
| "decision-note"
|
|
16
16
|
| "prompt-pattern"
|
|
17
|
-
| "source-summary"
|
|
17
|
+
| "source-summary"
|
|
18
|
+
| "idea-original"
|
|
19
|
+
| "person"
|
|
20
|
+
| "company-project"
|
|
21
|
+
| "meeting";
|
|
18
22
|
|
|
19
23
|
export interface NotePresetDefinition {
|
|
20
24
|
id: NotePresetId;
|
|
@@ -104,14 +108,14 @@ export const NOTE_PRESETS: NotePresetDefinition[] = [
|
|
|
104
108
|
{
|
|
105
109
|
id: "decision-note",
|
|
106
110
|
label: "Decision Note",
|
|
107
|
-
description: "
|
|
111
|
+
description: "Current synthesis, decision, rationale, and timeline.",
|
|
108
112
|
defaultTags: ["decision"],
|
|
109
113
|
frontmatter: {
|
|
110
114
|
category: "decision",
|
|
111
115
|
status: "proposed",
|
|
112
116
|
},
|
|
113
117
|
body: (title) =>
|
|
114
|
-
`# ${title}\n\n##
|
|
118
|
+
`# ${title}\n\n## Current Synthesis\n\n## Decision\n\n## Rationale\n\n## Consequences\n\n## Timeline\n`,
|
|
115
119
|
},
|
|
116
120
|
{
|
|
117
121
|
id: "prompt-pattern",
|
|
@@ -127,14 +131,66 @@ export const NOTE_PRESETS: NotePresetDefinition[] = [
|
|
|
127
131
|
{
|
|
128
132
|
id: "source-summary",
|
|
129
133
|
label: "Source Summary",
|
|
130
|
-
description: "Summarize
|
|
134
|
+
description: "Summarize a source with synthesis above evidence timeline.",
|
|
131
135
|
defaultTags: ["source", "summary"],
|
|
132
136
|
frontmatter: {
|
|
133
137
|
category: "source-summary",
|
|
134
138
|
sources: [],
|
|
135
139
|
},
|
|
136
140
|
body: (title) =>
|
|
137
|
-
`# ${title}\n\n##
|
|
141
|
+
`# ${title}\n\n## Current Synthesis\n\n## Important Claims\n\n## Assessment\n\n## Timeline\n\n## Evidence / Quotes\n`,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "idea-original",
|
|
145
|
+
label: "Original Idea",
|
|
146
|
+
description:
|
|
147
|
+
"Capture exact phrasing, context, related concepts, and publish potential.",
|
|
148
|
+
frontmatter: {
|
|
149
|
+
type: "idea-original",
|
|
150
|
+
category: "idea-original",
|
|
151
|
+
tags: [],
|
|
152
|
+
},
|
|
153
|
+
body: (title) =>
|
|
154
|
+
`# ${title}\n\n## Current Synthesis\n\n## Open Threads\n\n## Assessment\n\n## Timeline\n\n## Exact Phrasing\n\n## Related Concepts\n`,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "person",
|
|
158
|
+
label: "Person",
|
|
159
|
+
description:
|
|
160
|
+
"Track current state, relationship, assessment, open threads, and timeline.",
|
|
161
|
+
frontmatter: {
|
|
162
|
+
type: "person",
|
|
163
|
+
category: "person",
|
|
164
|
+
tags: [],
|
|
165
|
+
},
|
|
166
|
+
body: (title) =>
|
|
167
|
+
`# ${title}\n\n## Current Synthesis\n\n## Open Threads\n\n## Assessment\n\n## Timeline\n\n## Relationship\n`,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "company-project",
|
|
171
|
+
label: "Company / Project",
|
|
172
|
+
description:
|
|
173
|
+
"Track state, changes, decisions, people, open threads, and timeline.",
|
|
174
|
+
frontmatter: {
|
|
175
|
+
type: "company-project",
|
|
176
|
+
category: "company-project",
|
|
177
|
+
tags: [],
|
|
178
|
+
},
|
|
179
|
+
body: (title) =>
|
|
180
|
+
`# ${title}\n\n## Current Synthesis\n\n## Open Threads\n\n## Assessment\n\n## Timeline\n\n## Decisions\n\n## People\n`,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "meeting",
|
|
184
|
+
label: "Meeting",
|
|
185
|
+
description:
|
|
186
|
+
"Put analysis above the timeline; keep transcript, notes, and actions below.",
|
|
187
|
+
frontmatter: {
|
|
188
|
+
type: "meeting",
|
|
189
|
+
category: "meeting",
|
|
190
|
+
tags: [],
|
|
191
|
+
},
|
|
192
|
+
body: (title) =>
|
|
193
|
+
`# ${title}\n\n## Current Synthesis\n\n## Open Threads\n\n## Assessment\n\n## Timeline\n\n## Transcript / Notes\n\n## Action Items\n`,
|
|
138
194
|
},
|
|
139
195
|
];
|
|
140
196
|
|