openwriter 0.1.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/dist/bin/pad.js +64 -0
- package/dist/client/assets/index-DNJs7lC-.js +205 -0
- package/dist/client/assets/index-WweytMO1.css +1 -0
- package/dist/client/index.html +16 -0
- package/dist/server/compact.js +214 -0
- package/dist/server/documents.js +230 -0
- package/dist/server/export-html-template.js +109 -0
- package/dist/server/export-routes.js +96 -0
- package/dist/server/gdoc-import.js +200 -0
- package/dist/server/git-sync.js +272 -0
- package/dist/server/helpers.js +87 -0
- package/dist/server/image-upload.js +55 -0
- package/dist/server/index.js +315 -0
- package/dist/server/link-routes.js +116 -0
- package/dist/server/markdown-parse.js +405 -0
- package/dist/server/markdown-serialize.js +263 -0
- package/dist/server/markdown.js +6 -0
- package/dist/server/mcp-client.js +37 -0
- package/dist/server/mcp.js +457 -0
- package/dist/server/plugin-loader.js +36 -0
- package/dist/server/plugin-types.js +5 -0
- package/dist/server/state.js +749 -0
- package/dist/server/sync-routes.js +75 -0
- package/dist/server/text-edit.js +249 -0
- package/dist/server/version-routes.js +79 -0
- package/dist/server/versions.js +198 -0
- package/dist/server/workspace-routes.js +176 -0
- package/dist/server/workspace-tags.js +33 -0
- package/dist/server/workspace-tree.js +200 -0
- package/dist/server/workspace-types.js +38 -0
- package/dist/server/workspaces.js +257 -0
- package/dist/server/ws.js +211 -0
- package/package.json +88 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express routes for workspace CRUD, doc/container/tag operations.
|
|
3
|
+
* Mounted in index.ts to keep the main file lean.
|
|
4
|
+
*/
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
import { listWorkspaces, getWorkspace, createWorkspace, deleteWorkspace, reorderWorkspaces, addDoc, removeDoc, moveDoc, reorderDoc, addContainerToWorkspace, removeContainer, renameContainer, reorderContainer, tagDoc, untagDoc, } from './workspaces.js';
|
|
7
|
+
export function createWorkspaceRouter(b) {
|
|
8
|
+
const router = Router();
|
|
9
|
+
router.get('/api/workspaces', (_req, res) => {
|
|
10
|
+
res.json(listWorkspaces());
|
|
11
|
+
});
|
|
12
|
+
router.post('/api/workspaces', (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const result = createWorkspace({
|
|
15
|
+
title: req.body.title,
|
|
16
|
+
voiceProfileId: req.body.voiceProfileId,
|
|
17
|
+
});
|
|
18
|
+
b.broadcastWorkspacesChanged();
|
|
19
|
+
res.json(result);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
res.status(400).json({ error: err.message });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
router.get('/api/workspaces/:filename', (req, res) => {
|
|
26
|
+
try {
|
|
27
|
+
res.json(getWorkspace(req.params.filename));
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
res.status(404).json({ error: err.message });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
router.delete('/api/workspaces/:filename', (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
deleteWorkspace(req.params.filename);
|
|
36
|
+
b.broadcastWorkspacesChanged();
|
|
37
|
+
res.json({ success: true });
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
res.status(400).json({ error: err.message });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
router.put('/api/workspaces/reorder', (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const { order } = req.body;
|
|
46
|
+
if (!Array.isArray(order))
|
|
47
|
+
return res.status(400).json({ error: 'order must be an array' });
|
|
48
|
+
reorderWorkspaces(order);
|
|
49
|
+
b.broadcastWorkspacesChanged();
|
|
50
|
+
res.json({ success: true });
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
res.status(400).json({ error: err.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// Doc operations
|
|
57
|
+
router.post('/api/workspaces/:filename/docs', (req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
const ws = addDoc(req.params.filename, req.body.containerId ?? null, req.body.file, req.body.title || req.body.file, req.body.afterFile ?? null);
|
|
60
|
+
b.broadcastWorkspacesChanged();
|
|
61
|
+
res.json(ws);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
res.status(400).json({ error: err.message });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
router.delete('/api/workspaces/:filename/docs/:docFile', (req, res) => {
|
|
68
|
+
try {
|
|
69
|
+
const ws = removeDoc(req.params.filename, req.params.docFile);
|
|
70
|
+
b.broadcastWorkspacesChanged();
|
|
71
|
+
res.json(ws);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
res.status(400).json({ error: err.message });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
router.put('/api/workspaces/:filename/docs/:docFile/move', (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const ws = moveDoc(req.params.filename, req.params.docFile, req.body.targetContainerId ?? null, req.body.afterFile ?? null);
|
|
80
|
+
b.broadcastWorkspacesChanged();
|
|
81
|
+
res.json(ws);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
res.status(400).json({ error: err.message });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
router.put('/api/workspaces/:filename/docs/:docFile/reorder', (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const ws = reorderDoc(req.params.filename, req.params.docFile, req.body.afterFile ?? null);
|
|
90
|
+
b.broadcastWorkspacesChanged();
|
|
91
|
+
res.json(ws);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
res.status(400).json({ error: err.message });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// Container operations
|
|
98
|
+
router.post('/api/workspaces/:filename/containers', (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const result = addContainerToWorkspace(req.params.filename, req.body.parentContainerId ?? null, req.body.name);
|
|
101
|
+
b.broadcastWorkspacesChanged();
|
|
102
|
+
res.json(result);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
res.status(400).json({ error: err.message });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
router.delete('/api/workspaces/:filename/containers/:containerId', (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const ws = removeContainer(req.params.filename, req.params.containerId);
|
|
111
|
+
b.broadcastWorkspacesChanged();
|
|
112
|
+
res.json(ws);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
res.status(400).json({ error: err.message });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
router.put('/api/workspaces/:filename/containers/:containerId', (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
let ws;
|
|
121
|
+
if (req.body.name !== undefined) {
|
|
122
|
+
ws = renameContainer(req.params.filename, req.params.containerId, req.body.name);
|
|
123
|
+
}
|
|
124
|
+
b.broadcastWorkspacesChanged();
|
|
125
|
+
res.json(ws || getWorkspace(req.params.filename));
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
res.status(400).json({ error: err.message });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
router.put('/api/workspaces/:filename/containers/:containerId/reorder', (req, res) => {
|
|
132
|
+
try {
|
|
133
|
+
const ws = reorderContainer(req.params.filename, req.params.containerId, req.body.afterIdentifier ?? null);
|
|
134
|
+
b.broadcastWorkspacesChanged();
|
|
135
|
+
res.json(ws);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
res.status(400).json({ error: err.message });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// Tag operations
|
|
142
|
+
router.post('/api/workspaces/:filename/tags/:docFile', (req, res) => {
|
|
143
|
+
try {
|
|
144
|
+
const ws = tagDoc(req.params.filename, req.params.docFile, req.body.tag);
|
|
145
|
+
b.broadcastWorkspacesChanged();
|
|
146
|
+
res.json(ws);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
res.status(400).json({ error: err.message });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
router.delete('/api/workspaces/:filename/tags/:docFile/:tag', (req, res) => {
|
|
153
|
+
try {
|
|
154
|
+
const ws = untagDoc(req.params.filename, req.params.docFile, req.params.tag);
|
|
155
|
+
b.broadcastWorkspacesChanged();
|
|
156
|
+
res.json(ws);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
res.status(400).json({ error: err.message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// Cross-workspace move (from one workspace to another)
|
|
163
|
+
router.post('/api/workspaces/:targetFilename/docs/:docFile/cross-move', (req, res) => {
|
|
164
|
+
try {
|
|
165
|
+
const { sourceWorkspace } = req.body;
|
|
166
|
+
removeDoc(sourceWorkspace, req.params.docFile);
|
|
167
|
+
const ws = addDoc(req.params.targetFilename, req.body.containerId ?? null, req.params.docFile, req.body.title || req.params.docFile, req.body.afterFile ?? null);
|
|
168
|
+
b.broadcastWorkspacesChanged();
|
|
169
|
+
res.json(ws);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
res.status(400).json({ error: err.message });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return router;
|
|
176
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag index operations for workspace v2.
|
|
3
|
+
* Tags are a cross-cutting index: tag → [file1, file2, ...].
|
|
4
|
+
*/
|
|
5
|
+
export function addTag(tags, tagName, file) {
|
|
6
|
+
if (!tags[tagName])
|
|
7
|
+
tags[tagName] = [];
|
|
8
|
+
if (!tags[tagName].includes(file))
|
|
9
|
+
tags[tagName].push(file);
|
|
10
|
+
}
|
|
11
|
+
export function removeTag(tags, tagName, file) {
|
|
12
|
+
if (!tags[tagName])
|
|
13
|
+
return;
|
|
14
|
+
tags[tagName] = tags[tagName].filter((f) => f !== file);
|
|
15
|
+
if (tags[tagName].length === 0)
|
|
16
|
+
delete tags[tagName];
|
|
17
|
+
}
|
|
18
|
+
export function removeFileFromAllTags(tags, file) {
|
|
19
|
+
for (const tagName of Object.keys(tags)) {
|
|
20
|
+
removeTag(tags, tagName, file);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function listFilesForTag(tags, tagName) {
|
|
24
|
+
return tags[tagName] || [];
|
|
25
|
+
}
|
|
26
|
+
export function listTagsForFile(tags, file) {
|
|
27
|
+
const result = [];
|
|
28
|
+
for (const [tagName, files] of Object.entries(tags)) {
|
|
29
|
+
if (files.includes(file))
|
|
30
|
+
result.push(tagName);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure tree operations on WorkspaceNode[].
|
|
3
|
+
* All functions return mutated copies — caller writes to disk.
|
|
4
|
+
*/
|
|
5
|
+
import { generateContainerId } from './workspace-types.js';
|
|
6
|
+
const MAX_DEPTH = 3;
|
|
7
|
+
/** Recursive find — returns node, its parent array, and index within that array. */
|
|
8
|
+
export function findNode(nodes, predicate, parentRef) {
|
|
9
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
10
|
+
const node = nodes[i];
|
|
11
|
+
if (predicate(node)) {
|
|
12
|
+
return { node, parent: parentRef ?? nodes, index: i };
|
|
13
|
+
}
|
|
14
|
+
if (node.type === 'container') {
|
|
15
|
+
const found = findNode(node.items, predicate, node);
|
|
16
|
+
if (found)
|
|
17
|
+
return found;
|
|
18
|
+
}
|
|
19
|
+
if (node.type === 'doc' && node.children) {
|
|
20
|
+
for (const child of node.children) {
|
|
21
|
+
const found = findNode(child.items, predicate, child);
|
|
22
|
+
if (found)
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export function findDocNode(root, file) {
|
|
30
|
+
return findNode(root, (n) => n.type === 'doc' && n.file === file);
|
|
31
|
+
}
|
|
32
|
+
export function findContainer(root, containerId) {
|
|
33
|
+
return findNode(root, (n) => n.type === 'container' && n.id === containerId);
|
|
34
|
+
}
|
|
35
|
+
/** Get the items array that a container or root holds. */
|
|
36
|
+
function getItemsArray(root, containerId) {
|
|
37
|
+
if (containerId === null)
|
|
38
|
+
return root;
|
|
39
|
+
const found = findContainer(root, containerId);
|
|
40
|
+
if (!found)
|
|
41
|
+
return null;
|
|
42
|
+
return found.node.items;
|
|
43
|
+
}
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// DEPTH
|
|
46
|
+
// ============================================================================
|
|
47
|
+
/** Calculate the depth of a node in the tree. Root level = 0. */
|
|
48
|
+
export function getDepth(root, identifier) {
|
|
49
|
+
function walk(nodes, depth) {
|
|
50
|
+
for (const node of nodes) {
|
|
51
|
+
const id = node.type === 'doc' ? node.file : node.id;
|
|
52
|
+
if (id === identifier)
|
|
53
|
+
return depth;
|
|
54
|
+
if (node.type === 'container') {
|
|
55
|
+
const found = walk(node.items, depth + 1);
|
|
56
|
+
if (found >= 0)
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return -1;
|
|
61
|
+
}
|
|
62
|
+
return walk(root, 0);
|
|
63
|
+
}
|
|
64
|
+
/** Calculate the depth of a target container (where we'd insert into). */
|
|
65
|
+
function getContainerDepth(root, containerId) {
|
|
66
|
+
if (containerId === null)
|
|
67
|
+
return 0;
|
|
68
|
+
return getDepth(root, containerId) + 1;
|
|
69
|
+
}
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// ADD
|
|
72
|
+
// ============================================================================
|
|
73
|
+
export function addDocToContainer(root, containerId, file, title, afterIdentifier) {
|
|
74
|
+
// Check for duplicate
|
|
75
|
+
if (findDocNode(root, file)) {
|
|
76
|
+
throw new Error(`Document "${file}" already exists in workspace`);
|
|
77
|
+
}
|
|
78
|
+
const target = getItemsArray(root, containerId);
|
|
79
|
+
if (!target)
|
|
80
|
+
throw new Error(`Container "${containerId}" not found`);
|
|
81
|
+
const doc = { type: 'doc', file, title };
|
|
82
|
+
if (afterIdentifier) {
|
|
83
|
+
const afterIdx = target.findIndex((n) => (n.type === 'doc' && n.file === afterIdentifier) || (n.type === 'container' && n.id === afterIdentifier));
|
|
84
|
+
if (afterIdx === -1) {
|
|
85
|
+
target.push(doc);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
target.splice(afterIdx + 1, 0, doc);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
target.unshift(doc);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function addContainer(root, parentContainerId, name) {
|
|
96
|
+
const depth = getContainerDepth(root, parentContainerId);
|
|
97
|
+
if (depth >= MAX_DEPTH) {
|
|
98
|
+
throw new Error(`Maximum nesting depth (${MAX_DEPTH}) reached`);
|
|
99
|
+
}
|
|
100
|
+
const target = getItemsArray(root, parentContainerId);
|
|
101
|
+
if (!target)
|
|
102
|
+
throw new Error(`Parent container "${parentContainerId}" not found`);
|
|
103
|
+
const container = {
|
|
104
|
+
type: 'container',
|
|
105
|
+
id: generateContainerId(),
|
|
106
|
+
name,
|
|
107
|
+
items: [],
|
|
108
|
+
};
|
|
109
|
+
target.unshift(container);
|
|
110
|
+
return container;
|
|
111
|
+
}
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// REMOVE
|
|
114
|
+
// ============================================================================
|
|
115
|
+
/** Remove a node by file (doc) or id (container). Returns the removed node. */
|
|
116
|
+
export function removeNode(root, identifier) {
|
|
117
|
+
// Try as doc first, then as container
|
|
118
|
+
const found = findNode(root, (n) => (n.type === 'doc' && n.file === identifier) || (n.type === 'container' && n.id === identifier));
|
|
119
|
+
if (!found)
|
|
120
|
+
throw new Error(`Node "${identifier}" not found`);
|
|
121
|
+
const parentArray = found.parent instanceof Array ? found.parent : found.parent.items;
|
|
122
|
+
const [removed] = parentArray.splice(found.index, 1);
|
|
123
|
+
return removed;
|
|
124
|
+
}
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// MOVE / REORDER
|
|
127
|
+
// ============================================================================
|
|
128
|
+
/**
|
|
129
|
+
* Move a node to a different container (or root).
|
|
130
|
+
* afterIdentifier = null → insert at beginning; otherwise insert after that node.
|
|
131
|
+
*/
|
|
132
|
+
export function moveNode(root, identifier, targetContainerId, afterIdentifier) {
|
|
133
|
+
const removed = removeNode(root, identifier);
|
|
134
|
+
// Check depth for containers being moved
|
|
135
|
+
if (removed.type === 'container') {
|
|
136
|
+
const targetDepth = getContainerDepth(root, targetContainerId);
|
|
137
|
+
if (targetDepth >= MAX_DEPTH) {
|
|
138
|
+
// Re-add at root to not lose data, then throw
|
|
139
|
+
root.push(removed);
|
|
140
|
+
throw new Error(`Cannot move: would exceed max depth (${MAX_DEPTH})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const target = getItemsArray(root, targetContainerId);
|
|
144
|
+
if (!target) {
|
|
145
|
+
root.push(removed); // don't lose data
|
|
146
|
+
throw new Error(`Target container "${targetContainerId}" not found`);
|
|
147
|
+
}
|
|
148
|
+
if (afterIdentifier === null) {
|
|
149
|
+
target.unshift(removed);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const afterIdx = target.findIndex((n) => (n.type === 'doc' && n.file === afterIdentifier) || (n.type === 'container' && n.id === afterIdentifier));
|
|
153
|
+
if (afterIdx === -1) {
|
|
154
|
+
target.push(removed);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
target.splice(afterIdx + 1, 0, removed);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Reorder within the same parent. */
|
|
162
|
+
export function reorderNode(root, identifier, afterIdentifier) {
|
|
163
|
+
const found = findNode(root, (n) => (n.type === 'doc' && n.file === identifier) || (n.type === 'container' && n.id === identifier));
|
|
164
|
+
if (!found)
|
|
165
|
+
throw new Error(`Node "${identifier}" not found`);
|
|
166
|
+
const parentArray = found.parent instanceof Array ? found.parent : found.parent.items;
|
|
167
|
+
const [removed] = parentArray.splice(found.index, 1);
|
|
168
|
+
if (afterIdentifier === null) {
|
|
169
|
+
parentArray.unshift(removed);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const afterIdx = parentArray.findIndex((n) => (n.type === 'doc' && n.file === afterIdentifier) || (n.type === 'container' && n.id === afterIdentifier));
|
|
173
|
+
if (afterIdx === -1) {
|
|
174
|
+
parentArray.push(removed);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
parentArray.splice(afterIdx + 1, 0, removed);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// QUERIES
|
|
183
|
+
// ============================================================================
|
|
184
|
+
/** Collect all doc files in the tree. */
|
|
185
|
+
export function collectAllFiles(nodes) {
|
|
186
|
+
const files = [];
|
|
187
|
+
for (const node of nodes) {
|
|
188
|
+
if (node.type === 'doc') {
|
|
189
|
+
files.push(node.file);
|
|
190
|
+
}
|
|
191
|
+
else if (node.type === 'container') {
|
|
192
|
+
files.push(...collectAllFiles(node.items));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return files;
|
|
196
|
+
}
|
|
197
|
+
/** Count total doc items in the tree. */
|
|
198
|
+
export function countDocs(nodes) {
|
|
199
|
+
return collectAllFiles(nodes).length;
|
|
200
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace v2 type definitions and migration logic.
|
|
3
|
+
* Unified container model: ordered/unordered containers hold docs, tags are cross-cutting.
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// MIGRATION
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export function isV1(data) {
|
|
10
|
+
return !data.version || data.version < 2;
|
|
11
|
+
}
|
|
12
|
+
export function migrateV1toV2(legacy) {
|
|
13
|
+
const tags = {};
|
|
14
|
+
const root = [];
|
|
15
|
+
for (const item of legacy.items || []) {
|
|
16
|
+
root.push({ type: 'doc', file: item.file, title: item.file.replace(/\.md$/, '') });
|
|
17
|
+
if (item.tag) {
|
|
18
|
+
if (!tags[item.tag])
|
|
19
|
+
tags[item.tag] = [];
|
|
20
|
+
if (!tags[item.tag].includes(item.file))
|
|
21
|
+
tags[item.tag].push(item.file);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
version: 2,
|
|
26
|
+
title: legacy.title,
|
|
27
|
+
voiceProfileId: legacy.voiceProfileId ?? null,
|
|
28
|
+
root,
|
|
29
|
+
tags,
|
|
30
|
+
context: legacy.context,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// HELPERS
|
|
35
|
+
// ============================================================================
|
|
36
|
+
export function generateContainerId() {
|
|
37
|
+
return randomUUID().slice(0, 8);
|
|
38
|
+
}
|