studiograph 1.3.48-next.230 → 1.3.48-next.231
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/dist/core/field-library.d.ts +62 -0
- package/dist/core/field-library.js +129 -0
- package/dist/core/field-library.js.map +1 -0
- package/dist/mcp/comment-virtual-entity.d.ts +36 -0
- package/dist/mcp/comment-virtual-entity.js +71 -0
- package/dist/mcp/comment-virtual-entity.js.map +1 -0
- package/dist/server/routes/field-library-api.d.ts +28 -0
- package/dist/server/routes/field-library-api.js +126 -0
- package/dist/server/routes/field-library-api.js.map +1 -0
- package/dist/server/routes/graph-api.js +24 -0
- package/dist/server/routes/graph-api.js.map +1 -1
- package/dist/services/artifact-render-export.d.ts +16 -0
- package/dist/services/artifact-render-export.js +70 -0
- package/dist/services/artifact-render-export.js.map +1 -1
- package/dist/services/asset-upload-service.d.ts +56 -0
- package/dist/services/asset-upload-service.js +200 -0
- package/dist/services/asset-upload-service.js.map +1 -0
- package/dist/web/_app/immutable/chunks/{DPMW5NxI.js → -hm03jOI.js} +2 -2
- package/dist/web/_app/immutable/chunks/{DvHD5AC5.js → 3BtYsnOl.js} +1 -1
- package/dist/web/_app/immutable/chunks/{MIBhdB5N.js → B-eCsAwG.js} +1 -1
- package/dist/web/_app/immutable/chunks/{byAvLRUp.js → B1Yv3YkO.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BQXulat12.js → B3JpSBNe2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{hQpNl1sB.js → B7ikU8uG.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BMjF-1nt2.js → B8esiVhq2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BiC-31Lq.js → BADqghcB.js} +1 -1
- package/dist/web/_app/immutable/chunks/{jj_ctzfH2.js → BCCbgL3X2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{C9Aovnei.js → BJWwhYcQ.js} +2 -2
- package/dist/web/_app/immutable/chunks/{DENqOoRp.js → BN9TXN69.js} +1 -1
- package/dist/web/_app/immutable/chunks/{B3q77AoB.js → BNbFby5W.js} +1 -1
- package/dist/web/_app/immutable/chunks/{B4fk0_el.js → BW-sf0N4.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DSa8MhiI.js → BZ6p-6--.js} +1 -1
- package/dist/web/_app/immutable/chunks/{Df2wkqeG2.js → BZp7nJwe2.js} +2 -2
- package/dist/web/_app/immutable/chunks/{DeHjyziX.js → BZqoC7tF.js} +1 -1
- package/dist/web/_app/immutable/chunks/{B6lQfetb2.js → Bcb1m8Xc2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DcfccBKL.js → Bdz75Gen.js} +1 -1
- package/dist/web/_app/immutable/chunks/Bfn7xWpi.js +8 -0
- package/dist/web/_app/immutable/chunks/{CpCrFfxW.js → Biaf5vxK.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DLJiJEj_2.js → Bk8rX6M12.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BdaoSHCG.js → BpAhYe_M.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BbBk7cr-2.js → BvYk3gM92.js} +1 -1
- package/dist/web/_app/immutable/chunks/{9YGIzlTd.js → ByTixK1i.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DNlrU-ma.js → Bym3iIKr.js} +1 -1
- package/dist/web/_app/immutable/chunks/{B-NP12wx.js → C-rWXgyw.js} +2 -2
- package/dist/web/_app/immutable/chunks/{D9kQsNys.js → C04V7v3Y.js} +1 -1
- package/dist/web/_app/immutable/chunks/{cjgg_W1G2.js → C3CYo_IQ2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CzABfzc9.js → C4H0qFU9.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BE1eoOGk.js → C9QtJpOb.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CRafJm_e.js → C9frUVTC.js} +1 -1
- package/dist/web/_app/immutable/chunks/{hqv9I-Tl.js → CEJvXDkY.js} +2 -2
- package/dist/web/_app/immutable/chunks/{CR071Rle.js → CF2QFXIU.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CyuEKsyE.js → CQOrL1Xg.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DRkTZ8C1.js → CUG-KeXm.js} +1 -1
- package/dist/web/_app/immutable/chunks/Ca8AGdcM.js +1 -0
- package/dist/web/_app/immutable/chunks/{ByYqzsRB2.js → CaJRv9kC2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{Ca-KrO9X.js → Cb-QzQz3.js} +1 -1
- package/dist/web/_app/immutable/chunks/{lROhorp_2.js → CdAmGoAk2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BiWuH-Z1.js → Cpy0mGDa.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DBPa0PaY.js → Cu6j1_ld.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BE__qYcx.js → Cyv4P2rC.js} +1 -1
- package/dist/web/_app/immutable/chunks/{Bgj144iY.js → D4RhBc5W.js} +1 -1
- package/dist/web/_app/immutable/chunks/{B8hb7xMF.js → DLI5ShE_.js} +1 -1
- package/dist/web/_app/immutable/chunks/{Cvpe8bv9.js → DQllwhz4.js} +2 -2
- package/dist/web/_app/immutable/chunks/{BfgUbgAU.js → DR-cHvLq.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BHCM2gkM.js → DVdYVvQ_.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CVVMZVi5.js → DYmOKT8e.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DBINnYJ_2.js → DayPzYFE2.js} +2 -2
- package/dist/web/_app/immutable/chunks/{DEjsWhkg.js → Dr0PcvUI.js} +2 -2
- package/dist/web/_app/immutable/chunks/{DJf8bp6L.js → DuWMI5A2.js} +2 -2
- package/dist/web/_app/immutable/chunks/{teKuvRtH.js → DwZNoSMX.js} +1 -1
- package/dist/web/_app/immutable/chunks/{sScUK2tx2.js → DzWts1972.js} +1 -1
- package/dist/web/_app/immutable/chunks/{UJfaSsWC.js → GzZSf1zn.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DfWGZrU4.js → N0nIzSkb.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CQtBIY6W2.js → NYr8nVpL2.js} +2 -2
- package/dist/web/_app/immutable/chunks/{5eB-6tV5.js → RCBHRfcL.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BKTKjwqb2.js → T-v3aYaj2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{HByKAcP4.js → Y1ENS0GB.js} +1 -1
- package/dist/web/_app/immutable/chunks/_zekciZc.js +1 -0
- package/dist/web/_app/immutable/chunks/{DZsK5qtQ2.js → eTAE0Ol02.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CQBkrWgF.js → gfak5NLW.js} +1 -1
- package/dist/web/_app/immutable/chunks/{U7vv63j3.js → vKt5ZDh2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{jfosSc-4.js → vgzQgl7V.js} +3 -3
- package/dist/web/_app/immutable/chunks/{LyCWEn6z2.js → yKN1F5Ua2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{EuS1N3r92.js → z65JJtH62.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DNHmNHhy2.js → zPR_7qo62.js} +1 -1
- package/dist/web/_app/immutable/entry/{app.T-8gEdPs.js → app.DwFGv-tp.js} +2 -2
- package/dist/web/_app/immutable/entry/start.DS0SK0ki.js +1 -0
- package/dist/web/_app/immutable/nodes/{0.BtoFCZBg.js → 0.C_rQMpKf.js} +2 -2
- package/dist/web/_app/immutable/nodes/{1.bV_KWAHL.js → 1.CLTfs0pT.js} +1 -1
- package/dist/web/_app/immutable/nodes/{10.BUj2VY42.js → 10.2VRJN3EC.js} +1 -1
- package/dist/web/_app/immutable/nodes/{11.Cxrr9W8E.js → 11.Blu-tkNh.js} +1 -1
- package/dist/web/_app/immutable/nodes/{12.DZUuzmdz.js → 12.ZWUccwr_.js} +1 -1
- package/dist/web/_app/immutable/nodes/{13.D6BN-wHY.js → 13.B2WIt2M7.js} +1 -1
- package/dist/web/_app/immutable/nodes/{15.DmuL57i0.js → 15.BdHKCiq8.js} +1 -1
- package/dist/web/_app/immutable/nodes/{16.BiT3-n2j.js → 16.B1K8xlq5.js} +1 -1
- package/dist/web/_app/immutable/nodes/{17.nBtallJ_.js → 17.45hqPDVl.js} +1 -1
- package/dist/web/_app/immutable/nodes/{18.D-wPK1l4.js → 18.cJ2-Lx-Z.js} +1 -1
- package/dist/web/_app/immutable/nodes/{2.B9v6FCQK.js → 2.C8Fjf14Y.js} +4 -4
- package/dist/web/_app/immutable/nodes/{3.DYKS1dC-.js → 3.D5Y-v3kq.js} +3 -3
- package/dist/web/_app/immutable/nodes/4.xUjhWJDb.js +11 -0
- package/dist/web/_app/immutable/nodes/{5.AzhaE2X9.js → 5.0RArZ4ze.js} +1 -1
- package/dist/web/_app/immutable/nodes/{6.D0LVpnR0.js → 6.DnNumti8.js} +2 -2
- package/dist/web/_app/immutable/nodes/{7.DE9j2vwB.js → 7.Bthky8Lq.js} +1 -1
- package/dist/web/_app/immutable/nodes/{8.N-ozgnw5.js → 8.CIL8qye9.js} +1 -1
- package/dist/web/_app/immutable/nodes/{9.DWMm0RGr.js → 9.Bn5HGL4r.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +16 -16
- package/package.json +1 -1
- package/dist/web/_app/immutable/chunks/3WUkxtr0.js +0 -1
- package/dist/web/_app/immutable/chunks/BTI2ynas.js +0 -8
- package/dist/web/_app/immutable/chunks/BZb1RENj.js +0 -1
- package/dist/web/_app/immutable/entry/start.ivzkuEDO.js +0 -1
- package/dist/web/_app/immutable/nodes/4.Z7sWVUnL.js +0 -11
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field library mutation helpers
|
|
3
|
+
*
|
|
4
|
+
* Pure, framework-free functions that add / edit / remove a reusable custom
|
|
5
|
+
* field definition on a WorkspaceConfig. They return a new config object (the
|
|
6
|
+
* input is never mutated) so the HTTP layer can persist the result through
|
|
7
|
+
* `Workspace.writeConfig()`.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately independent of SchemaRegistry / Zod entity validation: the
|
|
10
|
+
* field library is *soft* metadata (it drives the picker + agent awareness),
|
|
11
|
+
* not an enforced schema. Applying a field to an entry is an ordinary
|
|
12
|
+
* frontmatter write elsewhere; nothing here touches entity validation.
|
|
13
|
+
*
|
|
14
|
+
* Identity is `key`, global across the workspace (one `phase` everywhere).
|
|
15
|
+
* Editing options ripples to every entry using the field; existing entry
|
|
16
|
+
* values are never touched (removal is non-destructive — orphaned values stay
|
|
17
|
+
* on disk and simply stop being offered).
|
|
18
|
+
*/
|
|
19
|
+
import { type CustomFieldDef, type WorkspaceConfig } from './types.js';
|
|
20
|
+
/** A mutation that fails a precondition (not found, collision, bad shape). */
|
|
21
|
+
export declare class FieldLibraryError extends Error {
|
|
22
|
+
/** Stable machine code so the route maps to a status without string-matching. */
|
|
23
|
+
code: 'key_exists' | 'key_not_found' | 'invalid_field';
|
|
24
|
+
constructor(message: string,
|
|
25
|
+
/** Stable machine code so the route maps to a status without string-matching. */
|
|
26
|
+
code: 'key_exists' | 'key_not_found' | 'invalid_field');
|
|
27
|
+
}
|
|
28
|
+
/** Current field library (empty when none defined). */
|
|
29
|
+
export declare function getFieldLibrary(config: WorkspaceConfig): CustomFieldDef[];
|
|
30
|
+
/** Look up one definition by key. */
|
|
31
|
+
export declare function getFieldDef(config: WorkspaceConfig, key: string): CustomFieldDef | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Validate a field definition's shape. Enforces the key shape and, for select
|
|
34
|
+
* fields, a non-empty list of unique, non-empty options. Throws
|
|
35
|
+
* FieldLibraryError on failure. Returns the parsed definition.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateFieldDef(input: unknown): CustomFieldDef;
|
|
38
|
+
/**
|
|
39
|
+
* Add a new field definition. Returns a new WorkspaceConfig.
|
|
40
|
+
* Rejects a key already used by another definition.
|
|
41
|
+
*/
|
|
42
|
+
export declare function addFieldDef(config: WorkspaceConfig, input: {
|
|
43
|
+
key: string;
|
|
44
|
+
label?: string;
|
|
45
|
+
type?: 'select';
|
|
46
|
+
options: string[];
|
|
47
|
+
created_by?: string;
|
|
48
|
+
}): WorkspaceConfig;
|
|
49
|
+
/**
|
|
50
|
+
* Update an existing definition's options and/or label. The key is the
|
|
51
|
+
* identity and is not changed (renaming a key = add + remove, since it would
|
|
52
|
+
* orphan existing usages). Returns a new WorkspaceConfig.
|
|
53
|
+
*/
|
|
54
|
+
export declare function updateFieldDef(config: WorkspaceConfig, key: string, patch: {
|
|
55
|
+
options?: string[];
|
|
56
|
+
label?: string;
|
|
57
|
+
}): WorkspaceConfig;
|
|
58
|
+
/**
|
|
59
|
+
* Remove a definition from the library. Returns a new WorkspaceConfig.
|
|
60
|
+
* Non-destructive to entries — any stored values stay in their frontmatter.
|
|
61
|
+
*/
|
|
62
|
+
export declare function removeFieldDef(config: WorkspaceConfig, key: string): WorkspaceConfig;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field library mutation helpers
|
|
3
|
+
*
|
|
4
|
+
* Pure, framework-free functions that add / edit / remove a reusable custom
|
|
5
|
+
* field definition on a WorkspaceConfig. They return a new config object (the
|
|
6
|
+
* input is never mutated) so the HTTP layer can persist the result through
|
|
7
|
+
* `Workspace.writeConfig()`.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately independent of SchemaRegistry / Zod entity validation: the
|
|
10
|
+
* field library is *soft* metadata (it drives the picker + agent awareness),
|
|
11
|
+
* not an enforced schema. Applying a field to an entry is an ordinary
|
|
12
|
+
* frontmatter write elsewhere; nothing here touches entity validation.
|
|
13
|
+
*
|
|
14
|
+
* Identity is `key`, global across the workspace (one `phase` everywhere).
|
|
15
|
+
* Editing options ripples to every entry using the field; existing entry
|
|
16
|
+
* values are never touched (removal is non-destructive — orphaned values stay
|
|
17
|
+
* on disk and simply stop being offered).
|
|
18
|
+
*/
|
|
19
|
+
import { CustomFieldDefSchema } from './types.js';
|
|
20
|
+
/** A mutation that fails a precondition (not found, collision, bad shape). */
|
|
21
|
+
export class FieldLibraryError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
constructor(message,
|
|
24
|
+
/** Stable machine code so the route maps to a status without string-matching. */
|
|
25
|
+
code) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.name = 'FieldLibraryError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Field key shape: lowercase, snake_case, must start with a letter. Mirrors
|
|
32
|
+
* the client's `normalizedKey` normalization. */
|
|
33
|
+
const KEY_RE = /^[a-z][a-z0-9_]*$/;
|
|
34
|
+
/** Current field library (empty when none defined). */
|
|
35
|
+
export function getFieldLibrary(config) {
|
|
36
|
+
return config.field_library ?? [];
|
|
37
|
+
}
|
|
38
|
+
/** Look up one definition by key. */
|
|
39
|
+
export function getFieldDef(config, key) {
|
|
40
|
+
return getFieldLibrary(config).find(f => f.key === key);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate a field definition's shape. Enforces the key shape and, for select
|
|
44
|
+
* fields, a non-empty list of unique, non-empty options. Throws
|
|
45
|
+
* FieldLibraryError on failure. Returns the parsed definition.
|
|
46
|
+
*/
|
|
47
|
+
export function validateFieldDef(input) {
|
|
48
|
+
const parsed = CustomFieldDefSchema.safeParse(input);
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
throw new FieldLibraryError(`Invalid field definition: ${parsed.error.issues.map(i => i.message).join('; ')}`, 'invalid_field');
|
|
51
|
+
}
|
|
52
|
+
const def = parsed.data;
|
|
53
|
+
if (!KEY_RE.test(def.key)) {
|
|
54
|
+
throw new FieldLibraryError(`Field key "${def.key}" must be lowercase snake_case and start with a letter`, 'invalid_field');
|
|
55
|
+
}
|
|
56
|
+
validateOptions(def.options);
|
|
57
|
+
return def;
|
|
58
|
+
}
|
|
59
|
+
/** Shared option-list validation (non-empty, no blanks, unique). */
|
|
60
|
+
function validateOptions(options) {
|
|
61
|
+
if (options.length === 0) {
|
|
62
|
+
throw new FieldLibraryError('A Select field needs at least one option', 'invalid_field');
|
|
63
|
+
}
|
|
64
|
+
if (options.some(v => v.trim() === '')) {
|
|
65
|
+
throw new FieldLibraryError('Select options cannot be empty', 'invalid_field');
|
|
66
|
+
}
|
|
67
|
+
if (new Set(options).size !== options.length) {
|
|
68
|
+
throw new FieldLibraryError('Select options must be unique', 'invalid_field');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Deep-clone the library so callers never mutate the live config. */
|
|
72
|
+
function cloneLibrary(config) {
|
|
73
|
+
return structuredClone(config.field_library ?? []);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Add a new field definition. Returns a new WorkspaceConfig.
|
|
77
|
+
* Rejects a key already used by another definition.
|
|
78
|
+
*/
|
|
79
|
+
export function addFieldDef(config, input) {
|
|
80
|
+
const def = validateFieldDef({
|
|
81
|
+
key: input.key,
|
|
82
|
+
type: 'select',
|
|
83
|
+
options: input.options,
|
|
84
|
+
...(input.label !== undefined ? { label: input.label } : {}),
|
|
85
|
+
...(input.created_by !== undefined ? { created_by: input.created_by } : {}),
|
|
86
|
+
created_at: new Date().toISOString(),
|
|
87
|
+
});
|
|
88
|
+
if (getFieldDef(config, def.key)) {
|
|
89
|
+
throw new FieldLibraryError(`A field named "${def.key}" already exists`, 'key_exists');
|
|
90
|
+
}
|
|
91
|
+
const library = [...cloneLibrary(config), def];
|
|
92
|
+
return { ...config, field_library: library };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Update an existing definition's options and/or label. The key is the
|
|
96
|
+
* identity and is not changed (renaming a key = add + remove, since it would
|
|
97
|
+
* orphan existing usages). Returns a new WorkspaceConfig.
|
|
98
|
+
*/
|
|
99
|
+
export function updateFieldDef(config, key, patch) {
|
|
100
|
+
const current = getFieldDef(config, key);
|
|
101
|
+
if (!current) {
|
|
102
|
+
throw new FieldLibraryError(`No field named "${key}"`, 'key_not_found');
|
|
103
|
+
}
|
|
104
|
+
if (patch.options !== undefined)
|
|
105
|
+
validateOptions(patch.options);
|
|
106
|
+
const next = {
|
|
107
|
+
...current,
|
|
108
|
+
...(patch.options !== undefined ? { options: patch.options } : {}),
|
|
109
|
+
...(patch.label !== undefined ? { label: patch.label } : {}),
|
|
110
|
+
};
|
|
111
|
+
const library = cloneLibrary(config).map(f => (f.key === key ? next : f));
|
|
112
|
+
return { ...config, field_library: library };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Remove a definition from the library. Returns a new WorkspaceConfig.
|
|
116
|
+
* Non-destructive to entries — any stored values stay in their frontmatter.
|
|
117
|
+
*/
|
|
118
|
+
export function removeFieldDef(config, key) {
|
|
119
|
+
if (!getFieldDef(config, key)) {
|
|
120
|
+
throw new FieldLibraryError(`No field named "${key}"`, 'key_not_found');
|
|
121
|
+
}
|
|
122
|
+
const library = cloneLibrary(config).filter(f => f.key !== key);
|
|
123
|
+
// Drop the array entirely when it empties, to keep workspace.json tidy.
|
|
124
|
+
const next = { ...config, field_library: library };
|
|
125
|
+
if (library.length === 0)
|
|
126
|
+
delete next.field_library;
|
|
127
|
+
return next;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=field-library.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-library.js","sourceRoot":"","sources":["../../src/core/field-library.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CommentService } from '../services/comment-service.js';
|
|
2
|
+
/**
|
|
3
|
+
* Comments live in SQLite for throughput, but agents should see them as
|
|
4
|
+
* first-class typed entities just like notes, projects, etc. This adapter
|
|
5
|
+
* shapes a Comment row into the same `{ id, entityType, data, content }`
|
|
6
|
+
* envelope that file-backed entities use, so generic MCP tools (`get_entity`,
|
|
7
|
+
* `list_entities`, `find_related`) can return comments without per-type
|
|
8
|
+
* branches in the tool code itself.
|
|
9
|
+
*
|
|
10
|
+
* The shape mirrors what `wm.getGraph(repo).get(...)` returns from
|
|
11
|
+
* filesystem entities: `id` is the comment_id (used in MCP URIs), `entityType`
|
|
12
|
+
* is the literal ‘comment’, `data` is frontmatter-flat with comment metadata,
|
|
13
|
+
* and `content` is the comment body.
|
|
14
|
+
*/
|
|
15
|
+
export interface VirtualEntityEnvelope {
|
|
16
|
+
id: string;
|
|
17
|
+
entityType: 'comment';
|
|
18
|
+
data: Record<string, unknown>;
|
|
19
|
+
document: {
|
|
20
|
+
content: string;
|
|
21
|
+
wikilinks: string[];
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function getComment(commentService: CommentService, commentId: string): VirtualEntityEnvelope | null;
|
|
25
|
+
export declare function listComments(commentService: CommentService, opts?: {
|
|
26
|
+
repo?: string;
|
|
27
|
+
entryType?: string;
|
|
28
|
+
entryId?: string;
|
|
29
|
+
resolved?: boolean;
|
|
30
|
+
limit?: number;
|
|
31
|
+
}): VirtualEntityEnvelope[];
|
|
32
|
+
/**
|
|
33
|
+
* Comments anchored to a given entry — exposed as backlinks of that entry
|
|
34
|
+
* so `find_related` includes them naturally.
|
|
35
|
+
*/
|
|
36
|
+
export declare function findRelatedComments(commentService: CommentService, repo: string, entityType: string, entityId: string): VirtualEntityEnvelope[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { parseCellAnchor } from '../core/cell-anchor.js';
|
|
2
|
+
function toEnvelope(c) {
|
|
3
|
+
// For dataset cell comments the anchor is JSON in anchor_relpos — surface
|
|
4
|
+
// the column + row snapshot so an agent reading the comment as an entity can
|
|
5
|
+
// locate the cell, not just see the bare cell value.
|
|
6
|
+
const cell = parseCellAnchor(c.anchorRelpos);
|
|
7
|
+
return {
|
|
8
|
+
id: c.commentId,
|
|
9
|
+
entityType: 'comment',
|
|
10
|
+
data: {
|
|
11
|
+
entity_type: 'comment',
|
|
12
|
+
entity_id: c.commentId,
|
|
13
|
+
created_at: c.createdAt,
|
|
14
|
+
updated_at: c.updatedAt ?? c.createdAt,
|
|
15
|
+
created_by: c.authorDisplayName,
|
|
16
|
+
updated_by: c.authorDisplayName,
|
|
17
|
+
// Comment-specific fields. Naming matches the SQL schema for
|
|
18
|
+
// consistency with the public REST API.
|
|
19
|
+
repo: c.repo,
|
|
20
|
+
entry_type: c.entryType,
|
|
21
|
+
entry_id: c.entryId,
|
|
22
|
+
thread_id: c.threadId,
|
|
23
|
+
parent_id: c.parentId,
|
|
24
|
+
author_user_id: c.authorUserId,
|
|
25
|
+
author_role: c.authorRole,
|
|
26
|
+
anchor_quote: c.anchorQuote,
|
|
27
|
+
...(cell
|
|
28
|
+
? {
|
|
29
|
+
anchor_cell: {
|
|
30
|
+
column: cell.headerName,
|
|
31
|
+
column_index: cell.columnIndex,
|
|
32
|
+
...(cell.rowCells ? { row_cells: cell.rowCells } : {}),
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
: {}),
|
|
36
|
+
resolved_at: c.resolvedAt,
|
|
37
|
+
resolved_by_user_id: c.resolvedByUserId,
|
|
38
|
+
edited: c.edited === 1,
|
|
39
|
+
mentions: c.mentions.map(m => m.handle),
|
|
40
|
+
},
|
|
41
|
+
document: { content: c.body, wikilinks: extractWikilinks(c.body) },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const WIKILINK_RE = /\[\[([^\]|#]+)(?:\|[^\]]+)?\]\]/g;
|
|
45
|
+
function extractWikilinks(body) {
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const match of body.matchAll(WIKILINK_RE)) {
|
|
48
|
+
if (match[1])
|
|
49
|
+
out.push(match[1].trim());
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
export function getComment(commentService, commentId) {
|
|
54
|
+
const c = commentService.getById(commentId);
|
|
55
|
+
if (!c || c.deletedAt)
|
|
56
|
+
return null;
|
|
57
|
+
return toEnvelope(c);
|
|
58
|
+
}
|
|
59
|
+
export function listComments(commentService, opts = {}) {
|
|
60
|
+
return commentService.list(opts).map(toEnvelope);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Comments anchored to a given entry — exposed as backlinks of that entry
|
|
64
|
+
* so `find_related` includes them naturally.
|
|
65
|
+
*/
|
|
66
|
+
export function findRelatedComments(commentService, repo, entityType, entityId) {
|
|
67
|
+
return commentService
|
|
68
|
+
.list({ repo, entryType: entityType, entryId: entityId })
|
|
69
|
+
.map(toEnvelope);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=comment-virtual-entity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comment-virtual-entity.js","sourceRoot":"","sources":["../../src/mcp/comment-virtual-entity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field Library API routes
|
|
3
|
+
*
|
|
4
|
+
* CRUD over the workspace's reusable custom field definitions (the field
|
|
5
|
+
* library). A definition is *soft* metadata — it drives the field picker and
|
|
6
|
+
* agent awareness, but it never feeds SchemaRegistry, so applying a field is
|
|
7
|
+
* an ordinary entity frontmatter write and an off-list value is never
|
|
8
|
+
* rejected here.
|
|
9
|
+
*
|
|
10
|
+
* Auth: any authenticated member (no owner gate) — the global auth hook
|
|
11
|
+
* already rejects unauthenticated callers when users exist. Open mode / API
|
|
12
|
+
* key are admin-equivalent (no req.user), same as other routes.
|
|
13
|
+
*
|
|
14
|
+
* Persistence mirrors config-api: Workspace.writeConfig(next) →
|
|
15
|
+
* workspaceManager.reloadConfig() (re-reads workspace.json into the live
|
|
16
|
+
* in-memory config). No SchemaRegistry rebuild is needed — the library is not
|
|
17
|
+
* compiled into Zod.
|
|
18
|
+
*
|
|
19
|
+
* GET /api/field-library
|
|
20
|
+
* POST /api/field-library
|
|
21
|
+
* PUT /api/field-library/:key
|
|
22
|
+
* DELETE /api/field-library/:key
|
|
23
|
+
* GET /api/field-library/:key/usage?value=
|
|
24
|
+
*/
|
|
25
|
+
import type { FastifyInstance } from 'fastify';
|
|
26
|
+
import type { WorkspaceManager } from '../../core/workspace-manager.js';
|
|
27
|
+
import type { AuthService } from '../../services/auth-service.js';
|
|
28
|
+
export declare function registerFieldLibraryApiRoutes(fastify: FastifyInstance, workspaceManager: WorkspaceManager, _authService: AuthService): Promise<void>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field Library API routes
|
|
3
|
+
*
|
|
4
|
+
* CRUD over the workspace's reusable custom field definitions (the field
|
|
5
|
+
* library). A definition is *soft* metadata — it drives the field picker and
|
|
6
|
+
* agent awareness, but it never feeds SchemaRegistry, so applying a field is
|
|
7
|
+
* an ordinary entity frontmatter write and an off-list value is never
|
|
8
|
+
* rejected here.
|
|
9
|
+
*
|
|
10
|
+
* Auth: any authenticated member (no owner gate) — the global auth hook
|
|
11
|
+
* already rejects unauthenticated callers when users exist. Open mode / API
|
|
12
|
+
* key are admin-equivalent (no req.user), same as other routes.
|
|
13
|
+
*
|
|
14
|
+
* Persistence mirrors config-api: Workspace.writeConfig(next) →
|
|
15
|
+
* workspaceManager.reloadConfig() (re-reads workspace.json into the live
|
|
16
|
+
* in-memory config). No SchemaRegistry rebuild is needed — the library is not
|
|
17
|
+
* compiled into Zod.
|
|
18
|
+
*
|
|
19
|
+
* GET /api/field-library
|
|
20
|
+
* POST /api/field-library
|
|
21
|
+
* PUT /api/field-library/:key
|
|
22
|
+
* DELETE /api/field-library/:key
|
|
23
|
+
* GET /api/field-library/:key/usage?value=
|
|
24
|
+
*/
|
|
25
|
+
import { Workspace } from '../../core/workspace.js';
|
|
26
|
+
import { addFieldDef, updateFieldDef, removeFieldDef, getFieldDef, getFieldLibrary, FieldLibraryError, } from '../../core/field-library.js';
|
|
27
|
+
import { reqUser } from './graph-api-access.js';
|
|
28
|
+
function mapError(err) {
|
|
29
|
+
switch (err.code) {
|
|
30
|
+
case 'key_exists': return { status: 409, code: err.code };
|
|
31
|
+
case 'key_not_found': return { status: 404, code: err.code };
|
|
32
|
+
case 'invalid_field': return { status: 400, code: err.code };
|
|
33
|
+
default: return { status: 400, code: 'invalid_field' };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function registerFieldLibraryApiRoutes(fastify, workspaceManager, _authService) {
|
|
37
|
+
/** Persist a config mutation: write workspace.json + refresh the live config. */
|
|
38
|
+
async function persist(next) {
|
|
39
|
+
const workspace = new Workspace(workspaceManager.getWorkspacePath());
|
|
40
|
+
workspace.writeConfig(next);
|
|
41
|
+
workspaceManager.reloadConfig();
|
|
42
|
+
}
|
|
43
|
+
// GET /api/field-library — list all definitions.
|
|
44
|
+
fastify.get('/api/field-library', async (_req, reply) => {
|
|
45
|
+
return reply.send({ fields: getFieldLibrary(workspaceManager.getConfig()) });
|
|
46
|
+
});
|
|
47
|
+
// POST /api/field-library — define a new field.
|
|
48
|
+
fastify.post('/api/field-library', async (req, reply) => {
|
|
49
|
+
const { key, label, options } = req.body ?? {};
|
|
50
|
+
try {
|
|
51
|
+
const next = addFieldDef(workspaceManager.getConfig(), {
|
|
52
|
+
key,
|
|
53
|
+
options,
|
|
54
|
+
...(label !== undefined ? { label } : {}),
|
|
55
|
+
...(reqUser(req)?.handle ? { created_by: reqUser(req).handle } : {}),
|
|
56
|
+
});
|
|
57
|
+
await persist(next);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err instanceof FieldLibraryError) {
|
|
61
|
+
const { status, code } = mapError(err);
|
|
62
|
+
return reply.status(status).send({ code, error: err.message });
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
return reply.send({ ok: true, fields: getFieldLibrary(workspaceManager.getConfig()) });
|
|
67
|
+
});
|
|
68
|
+
// PUT /api/field-library/:key — edit options/label. Returns affected-entry
|
|
69
|
+
// counts for any option values dropped by the edit (orphan warning).
|
|
70
|
+
fastify.put('/api/field-library/:key', async (req, reply) => {
|
|
71
|
+
const { key } = req.params;
|
|
72
|
+
const body = req.body ?? {};
|
|
73
|
+
const before = getFieldDef(workspaceManager.getConfig(), key);
|
|
74
|
+
const affected = {};
|
|
75
|
+
if (before?.options && Array.isArray(body.options)) {
|
|
76
|
+
const nextSet = new Set(body.options);
|
|
77
|
+
for (const v of before.options) {
|
|
78
|
+
if (!nextSet.has(v))
|
|
79
|
+
affected[v] = workspaceManager.countEntitiesWithFieldValue(key, v);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const next = updateFieldDef(workspaceManager.getConfig(), key, {
|
|
84
|
+
...(body.options !== undefined ? { options: body.options } : {}),
|
|
85
|
+
...(body.label !== undefined ? { label: body.label } : {}),
|
|
86
|
+
});
|
|
87
|
+
await persist(next);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err instanceof FieldLibraryError) {
|
|
91
|
+
const { status, code } = mapError(err);
|
|
92
|
+
return reply.status(status).send({ code, error: err.message });
|
|
93
|
+
}
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
return reply.send({ ok: true, fields: getFieldLibrary(workspaceManager.getConfig()), affected });
|
|
97
|
+
});
|
|
98
|
+
// DELETE /api/field-library/:key — remove the definition. Entry values are
|
|
99
|
+
// left on disk. Returns the count of entries that still held the field.
|
|
100
|
+
fastify.delete('/api/field-library/:key', async (req, reply) => {
|
|
101
|
+
const { key } = req.params;
|
|
102
|
+
const affected = workspaceManager.countEntitiesWithFieldValue(key);
|
|
103
|
+
try {
|
|
104
|
+
const next = removeFieldDef(workspaceManager.getConfig(), key);
|
|
105
|
+
await persist(next);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (err instanceof FieldLibraryError) {
|
|
109
|
+
const { status, code } = mapError(err);
|
|
110
|
+
return reply.status(status).send({ code, error: err.message });
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
return reply.send({ ok: true, fields: getFieldLibrary(workspaceManager.getConfig()), affected });
|
|
115
|
+
});
|
|
116
|
+
// GET /api/field-library/:key/usage?value= — entry counts behind the
|
|
117
|
+
// orphan-warning before an option/field removal.
|
|
118
|
+
fastify.get('/api/field-library/:key/usage', async (req, reply) => {
|
|
119
|
+
const { key } = req.params;
|
|
120
|
+
const { value } = req.query;
|
|
121
|
+
const total = workspaceManager.countEntitiesWithFieldValue(key);
|
|
122
|
+
const forValue = value !== undefined ? workspaceManager.countEntitiesWithFieldValue(key, value) : undefined;
|
|
123
|
+
return reply.send({ total, ...(forValue !== undefined ? { value, count: forValue } : {}) });
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=field-library-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-library-api.js","sourceRoot":"","sources":["../../../src/server/routes/field-library-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -388,6 +388,30 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
388
388
|
}
|
|
389
389
|
return reply.send(entity);
|
|
390
390
|
});
|
|
391
|
+
// GET /api/repos/:repo/entities/:type/:id/source — download the raw
|
|
392
|
+
// markdown file (frontmatter + body verbatim, exactly as on disk). For
|
|
393
|
+
// artifacts the body IS the embedded bundle JSON, so the download is a
|
|
394
|
+
// portable single-file copy of the whole entry. Read-gated like every
|
|
395
|
+
// other entity read; serialization fidelity comes from serving the disk
|
|
396
|
+
// bytes rather than re-emitting parsed frontmatter.
|
|
397
|
+
fastify.get('/api/repos/:repo/entities/:type/:id/source', async (req, reply) => {
|
|
398
|
+
const { repo, type, id } = req.params;
|
|
399
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
400
|
+
const graph = graphForRead(req, reply, repo);
|
|
401
|
+
if (!graph)
|
|
402
|
+
return;
|
|
403
|
+
const filePath = graph.getEntityFilePath(type, id);
|
|
404
|
+
if (!filePath || !existsSync(filePath)) {
|
|
405
|
+
return reply.status(404).send({ error: `Entity not found: ${type}/${id}` });
|
|
406
|
+
}
|
|
407
|
+
const raw = readFileSync(filePath);
|
|
408
|
+
// Entity ids are kebab-case slugs (ASCII-safe), so the quoted filename
|
|
409
|
+
// needs no RFC 5987 escaping.
|
|
410
|
+
reply.header('Content-Disposition', `attachment; filename="${id}.md"`);
|
|
411
|
+
reply.header('Content-Type', 'text/markdown; charset=utf-8');
|
|
412
|
+
reply.header('X-Content-Type-Options', 'nosniff');
|
|
413
|
+
return reply.send(raw);
|
|
414
|
+
});
|
|
391
415
|
// GET /api/repos/:repo/entities/:type/:id/history — entity commit history
|
|
392
416
|
fastify.get('/api/repos/:repo/entities/:type/:id/history', async (req, reply) => {
|
|
393
417
|
const { repo, type, id } = req.params;
|