drafted 1.11.23 → 1.11.24
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/mcp/active-project-store.mjs +63 -0
- package/mcp/server.mjs +35 -4
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active-project persistence for the stdio MCP.
|
|
3
|
+
*
|
|
4
|
+
* The active project lives only in the in-memory sessionStates Map, so a
|
|
5
|
+
* `system restart mode=mcp` / skill-edit pass that re-spawns the stdio MCP
|
|
6
|
+
* wipes it — the recurring "active project keeps resetting, re-open it" churn.
|
|
7
|
+
* Persisting it lets the long-lived stdio process rehydrate transparently after
|
|
8
|
+
* a restart.
|
|
9
|
+
*
|
|
10
|
+
* Keyed by cwd so two concurrent same-machine agent sessions (each its own
|
|
11
|
+
* stdio MCP, same launch config) don't clobber each other's active project.
|
|
12
|
+
* Only the stdio process persists here; HTTP/remote user sessions are keyed by
|
|
13
|
+
* a real Drafted session id and never call into this store.
|
|
14
|
+
*
|
|
15
|
+
* Side-effect-free on import (no network, no eager writes) so it can be unit
|
|
16
|
+
* tested against the real code without booting the MCP server.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
19
|
+
import { join, dirname } from 'path';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_FILE = process.env.DRAFTED_MCP_STATE_FILE || join(homedir(), '.drafted', 'mcp-state.json');
|
|
23
|
+
|
|
24
|
+
function defaultCwd() {
|
|
25
|
+
try { return process.cwd(); } catch { return '__default__'; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadPersistedProject({ file = DEFAULT_FILE, cwd = defaultCwd() } = {}) {
|
|
29
|
+
try {
|
|
30
|
+
const all = JSON.parse(readFileSync(file, 'utf8'));
|
|
31
|
+
const e = all && all[cwd];
|
|
32
|
+
if (e && e.activeProjectId) {
|
|
33
|
+
return {
|
|
34
|
+
activeProjectId: e.activeProjectId,
|
|
35
|
+
activeProjectMeta: e.activeProjectMeta || null,
|
|
36
|
+
boundOrgId: e.boundOrgId || null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
} catch { /* no state file yet / unreadable — start fresh */ }
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function savePersistedProject(entry, { file = DEFAULT_FILE, cwd = defaultCwd() } = {}) {
|
|
44
|
+
try {
|
|
45
|
+
let all = {};
|
|
46
|
+
try { all = JSON.parse(readFileSync(file, 'utf8')) || {}; } catch { /* recreate */ }
|
|
47
|
+
if (entry && entry.activeProjectId) {
|
|
48
|
+
all[cwd] = {
|
|
49
|
+
activeProjectId: entry.activeProjectId,
|
|
50
|
+
activeProjectMeta: entry.activeProjectMeta || null,
|
|
51
|
+
boundOrgId: entry.boundOrgId || null,
|
|
52
|
+
};
|
|
53
|
+
} else {
|
|
54
|
+
delete all[cwd];
|
|
55
|
+
}
|
|
56
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
57
|
+
writeFileSync(file, JSON.stringify(all), { mode: 0o600 });
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
// Best-effort; persistence is an optimization, never block a tool on it.
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
package/mcp/server.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import WebSocket from 'ws';
|
|
|
21
21
|
import { LAYERS } from '../src/shared/constants.mjs';
|
|
22
22
|
import { emptyExcalidrawScene, excalidrawSceneFromMermaid, stringifyExcalidrawScene } from '../src/shared/excalidraw.mjs';
|
|
23
23
|
import { createGateState, markSearched, g1Block, g2Block, g3Block, selectWithinBudget, wouldExceedBudget, budgetError, formatWikiIndex, PROJECT_CONTEXT_BUDGET_CHARS } from './gates.mjs';
|
|
24
|
+
import { loadPersistedProject, savePersistedProject } from './active-project-store.mjs';
|
|
24
25
|
|
|
25
26
|
// Frame actions that mutate content — gated by G1 (wiki search before editing).
|
|
26
27
|
// Read-style actions (read, search, versions, get_*/read_*) are exempt.
|
|
@@ -92,6 +93,9 @@ function getOrCreateSessionState(sid) {
|
|
|
92
93
|
cachedOrgIdTime: 0,
|
|
93
94
|
wsSessionId: null,
|
|
94
95
|
pendingDeviceCode: null,
|
|
96
|
+
// Only the long-lived stdio bucket persists its active project to disk;
|
|
97
|
+
// HTTP/remote sessions (keyed by a real Drafted session id) must not.
|
|
98
|
+
_stdio: key === '__stdio__',
|
|
95
99
|
};
|
|
96
100
|
sessionStates.set(key, s);
|
|
97
101
|
}
|
|
@@ -439,6 +443,28 @@ registerAppResource(
|
|
|
439
443
|
const AUTH_FILE = process.env.DRAFTED_AUTH_FILE || join(homedir(), '.drafted', 'auth.json');
|
|
440
444
|
const PENDING_AUTH_FILE = process.env.DRAFTED_PENDING_AUTH_FILE || `${AUTH_FILE}.pending`;
|
|
441
445
|
|
|
446
|
+
// Active-project persistence (stdio only): see mcp/active-project-store.mjs.
|
|
447
|
+
// Persisted so a `system restart mode=mcp` / skill-edit pass that re-spawns the
|
|
448
|
+
// stdio MCP rehydrates the active project instead of dropping it (the recurring
|
|
449
|
+
// "active project keeps resetting" churn). Keyed by cwd; HTTP/remote sessions
|
|
450
|
+
// (real Drafted session id, never the __stdio__ bucket) never touch this file.
|
|
451
|
+
|
|
452
|
+
// Rehydrate the stdio MCP's active project at process start so the first tool
|
|
453
|
+
// call after a restart is already scoped. Seeds both standaloneState (what
|
|
454
|
+
// api() reads for projectId scoping in stdio mode) and the __stdio__ session
|
|
455
|
+
// bucket (boundOrgId → the X-Drafted-Org per-request scope). No-op when the
|
|
456
|
+
// file is absent (fresh install, or the hosted server process).
|
|
457
|
+
(function rehydrateStdioActiveProject() {
|
|
458
|
+
const p = loadPersistedProject();
|
|
459
|
+
if (!p) { getOrCreateSessionState(null); return; }
|
|
460
|
+
standaloneState.projectId = p.activeProjectId;
|
|
461
|
+
standaloneState.projectMeta = p.activeProjectMeta || null;
|
|
462
|
+
const sess = getOrCreateSessionState(null);
|
|
463
|
+
sess.activeProjectId = p.activeProjectId;
|
|
464
|
+
sess.activeProjectMeta = p.activeProjectMeta || null;
|
|
465
|
+
if (p.boundOrgId) sess.boundOrgId = p.boundOrgId;
|
|
466
|
+
})();
|
|
467
|
+
|
|
442
468
|
function getServerUrl() {
|
|
443
469
|
if (process.env.DRAFTED_SERVER) return process.env.DRAFTED_SERVER.replace(/\/$/, '');
|
|
444
470
|
if (getState().publicUrl) return getState().publicUrl.replace(/\/$/, '');
|
|
@@ -835,6 +861,7 @@ async function api(method, path, body, extraHeaders = {}, _retried = false) {
|
|
|
835
861
|
const sess = getSessionState();
|
|
836
862
|
sess.activeProjectId = null;
|
|
837
863
|
sess.activeProjectMeta = null;
|
|
864
|
+
if (sess._stdio) savePersistedProject(null);
|
|
838
865
|
throw new Error(
|
|
839
866
|
`${msg} — the active project (${meta?.slug || pid}) is no longer in the current org. ` +
|
|
840
867
|
`Active project cleared. Call project(action="open") to set a new one, or proceed without one for org-scoped tools (wiki, skill).`
|
|
@@ -979,6 +1006,10 @@ function setMcpActiveProject(projectId, meta = null) {
|
|
|
979
1006
|
// inheriting the root login's current org. Only set when known — a (null,null)
|
|
980
1007
|
// clear must NOT wipe the bound org (the switch handler sets it explicitly).
|
|
981
1008
|
if (meta?.orgId) sess.boundOrgId = meta.orgId;
|
|
1009
|
+
// Persist for the stdio process so an MCP restart rehydrates this project.
|
|
1010
|
+
if (sess._stdio) {
|
|
1011
|
+
savePersistedProject(projectId ? { activeProjectId: projectId, activeProjectMeta: meta, boundOrgId: sess.boundOrgId } : null);
|
|
1012
|
+
}
|
|
982
1013
|
}
|
|
983
1014
|
|
|
984
1015
|
// Returns { id, slug, name, orgId } for the project this MCP session most
|
|
@@ -2057,7 +2088,7 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
2057
2088
|
color: z.string().optional().describe('[write] CSS color for frame border (e.g. #ff0000, red).'),
|
|
2058
2089
|
operations: z.array(z.object({
|
|
2059
2090
|
type: z.enum(['replace', 'delete', 'insertAfter', 'insertBefore']).describe('Edit type'),
|
|
2060
|
-
lineHash: z.string().describe('The full line anchor copied verbatim from read output — line number +
|
|
2091
|
+
lineHash: z.string().describe('The full line anchor copied verbatim from read output — line number + 3-char hash, e.g. "182vix" (the token left of the "|"). NOT the bare hash "vix": that is rejected, since the same hash can recur on multiple lines.'),
|
|
2061
2092
|
newContent: z.string().optional().describe('New content (for replace, insertAfter, insertBefore)'),
|
|
2062
2093
|
})).optional().describe('[edit] hashline edit operations'),
|
|
2063
2094
|
from: z.string().optional().describe('[mv] source path /{layer}/{lane}/{filename}'),
|
|
@@ -2576,9 +2607,9 @@ tool('batch', 'Batch operations on the ACTIVE PROJECT. Response includes "projec
|
|
|
2576
2607
|
to: z.string().optional().describe('Destination path (for mv)'),
|
|
2577
2608
|
operations: z.array(z.object({
|
|
2578
2609
|
type: z.enum(['replace', 'delete', 'insertAfter', 'insertBefore']),
|
|
2579
|
-
lineHash: z.string().describe('The full line anchor from read output — line number +
|
|
2610
|
+
lineHash: z.string().describe('The full line anchor from read output — line number + 3-char hash, e.g. "182vix". NOT the bare hash.'),
|
|
2580
2611
|
newContent: z.string().optional(),
|
|
2581
|
-
})).optional().describe('Edit operations (for edit). lineHash is the full anchor (lineNum + hash), e.g. "
|
|
2612
|
+
})).optional().describe('Edit operations (for edit). lineHash is the full anchor (lineNum + hash), e.g. "182vix".'),
|
|
2582
2613
|
asset_path: z.string().optional().describe('Relative asset path (for upload_asset, e.g., "css/styles.css")'),
|
|
2583
2614
|
content_type: z.string().optional().describe('MIME type (for upload_asset, auto-detected if omitted)'),
|
|
2584
2615
|
frame_id: z.string().optional().describe('Frame ID to associate asset with (for upload_asset)'),
|
|
@@ -3135,7 +3166,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3135
3166
|
frontmatter: z.any().optional().describe('[write] frontmatter object'),
|
|
3136
3167
|
operations: z.array(z.object({
|
|
3137
3168
|
type: z.enum(['replace', 'delete', 'insertAfter', 'insertBefore']).describe('Edit type'),
|
|
3138
|
-
lineHash: z.string().describe('The full line anchor copied verbatim from read output — line number +
|
|
3169
|
+
lineHash: z.string().describe('The full line anchor copied verbatim from read output — line number + 3-char hash, e.g. "182vix" (the token left of the "|"). NOT the bare hash.'),
|
|
3139
3170
|
newContent: z.string().optional().describe('New content (for replace, insertAfter, insertBefore)'),
|
|
3140
3171
|
})).optional().describe('[edit] hashline edit operations — same shape as frame.edit'),
|
|
3141
3172
|
from: z.string().optional().describe('[mv] source path'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.24",
|
|
4
4
|
"description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|