@drawsy/companion 0.0.0-stage → 0.1.28
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/LICENSE +21 -0
- package/README.md +98 -2
- package/dist/drawsy/bridge-main.js +15 -0
- package/dist/drawsy/bridge.js +1794 -0
- package/dist/drawsy/cli-autostart.js +201 -0
- package/dist/drawsy/cli.js +303 -0
- package/dist/drawsy/codex-app-server.js +2199 -0
- package/dist/drawsy/codex-binary.js +26 -0
- package/dist/drawsy/connector-backend.js +57 -0
- package/dist/drawsy/conversation-history.js +35 -0
- package/dist/drawsy/engine-status.js +20 -0
- package/dist/drawsy/environment.js +17 -0
- package/dist/drawsy/executable-resolver.js +95 -0
- package/dist/drawsy/folder-picker.js +66 -0
- package/dist/drawsy/image-asset.js +173 -0
- package/dist/drawsy/local-conversation-store.js +241 -0
- package/dist/drawsy/mcp-launcher.js +15 -0
- package/dist/drawsy/mcp.js +31588 -0
- package/dist/drawsy/opencode-app-server.js +1251 -0
- package/dist/drawsy/opencode-binary.js +26 -0
- package/dist/drawsy/protocol.js +519 -0
- package/package.json +40 -4
- package/skills/drawsy-browser-use/SKILL.md +128 -0
- package/skills/drawsy-browser-use/references/drawsy-tool-catalog.md +90 -0
- package/skills/drawsy-fast-diagrams/SKILL.md +15 -0
- package/skills/drawsy-teaching-diagrams/SKILL.md +117 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const emptyPreference = () => ({
|
|
5
|
+
model: null,
|
|
6
|
+
modelProvider: null,
|
|
7
|
+
effort: null,
|
|
8
|
+
accessMode: null,
|
|
9
|
+
internetEnabled: null
|
|
10
|
+
});
|
|
11
|
+
export const defaultConversationPreferences = () => ({
|
|
12
|
+
engine: "codex",
|
|
13
|
+
codex: emptyPreference(),
|
|
14
|
+
opencode: emptyPreference(),
|
|
15
|
+
updatedAt: 0
|
|
16
|
+
});
|
|
17
|
+
const initialStore = () => ({
|
|
18
|
+
version: 1,
|
|
19
|
+
preferences: defaultConversationPreferences(),
|
|
20
|
+
conversations: [],
|
|
21
|
+
rememberedFolders: {}
|
|
22
|
+
});
|
|
23
|
+
const localStateDirectory = () => {
|
|
24
|
+
if (process.env.DRAWSY_LOCAL_STATE_DIR) {
|
|
25
|
+
return path.resolve(process.env.DRAWSY_LOCAL_STATE_DIR);
|
|
26
|
+
}
|
|
27
|
+
if (process.platform === "darwin") {
|
|
28
|
+
return path.join(homedir(), "Library", "Application Support", "Drawsy AI");
|
|
29
|
+
}
|
|
30
|
+
return path.join(process.env.XDG_STATE_HOME || path.join(homedir(), ".local", "state"), "drawsy-ai");
|
|
31
|
+
};
|
|
32
|
+
const isAgentEngine = (value) => value === "codex" || value === "opencode";
|
|
33
|
+
const isAccessMode = (value) => value === "workspace" || value === "readOnly";
|
|
34
|
+
const readPreference = (value) => {
|
|
35
|
+
const record = value && typeof value === "object" ? value : {};
|
|
36
|
+
return {
|
|
37
|
+
model: typeof record.model === "string" ? record.model : null,
|
|
38
|
+
modelProvider: typeof record.modelProvider === "string" ? record.modelProvider : null,
|
|
39
|
+
effort: typeof record.effort === "string" ? record.effort : null,
|
|
40
|
+
accessMode: isAccessMode(record.accessMode) ? record.accessMode : null,
|
|
41
|
+
internetEnabled: typeof record.internetEnabled === "boolean" ? record.internetEnabled : null
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const readConversation = (value) => {
|
|
45
|
+
const record = value && typeof value === "object" ? value : null;
|
|
46
|
+
if (!record ||
|
|
47
|
+
typeof record.id !== "string" ||
|
|
48
|
+
(record.scope !== "canvas" && record.scope !== "general") ||
|
|
49
|
+
!isAgentEngine(record.engine) ||
|
|
50
|
+
typeof record.title !== "string" ||
|
|
51
|
+
typeof record.createdAt !== "number" ||
|
|
52
|
+
typeof record.updatedAt !== "number" ||
|
|
53
|
+
typeof record.messageCount !== "number") {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
id: record.id,
|
|
58
|
+
scope: record.scope,
|
|
59
|
+
canvasId: typeof record.canvasId === "string" ? record.canvasId : null,
|
|
60
|
+
canvasName: typeof record.canvasName === "string" ? record.canvasName : null,
|
|
61
|
+
engine: record.engine,
|
|
62
|
+
title: record.title,
|
|
63
|
+
createdAt: record.createdAt,
|
|
64
|
+
updatedAt: record.updatedAt,
|
|
65
|
+
messageCount: Math.max(0, Math.floor(record.messageCount)),
|
|
66
|
+
workspacePath: typeof record.workspacePath === "string" ? record.workspacePath : null,
|
|
67
|
+
codexThreadId: typeof record.codexThreadId === "string" ? record.codexThreadId : null,
|
|
68
|
+
openCodeSessionId: typeof record.openCodeSessionId === "string" ? record.openCodeSessionId : null
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
export class LocalConversationStore {
|
|
72
|
+
stateDirectory = localStateDirectory();
|
|
73
|
+
statePath = path.join(this.stateDirectory, "ai-conversations.json");
|
|
74
|
+
data = initialStore();
|
|
75
|
+
loaded;
|
|
76
|
+
writeQueue = Promise.resolve();
|
|
77
|
+
constructor() {
|
|
78
|
+
this.loaded = this.load();
|
|
79
|
+
}
|
|
80
|
+
async load() {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(await readFile(this.statePath, "utf8"));
|
|
83
|
+
const record = parsed && typeof parsed === "object" ? parsed : null;
|
|
84
|
+
if (!record || record.version !== 1)
|
|
85
|
+
return;
|
|
86
|
+
const preferencesRecord = record.preferences && typeof record.preferences === "object"
|
|
87
|
+
? record.preferences
|
|
88
|
+
: {};
|
|
89
|
+
const preferences = {
|
|
90
|
+
engine: isAgentEngine(preferencesRecord.engine) ? preferencesRecord.engine : "codex",
|
|
91
|
+
codex: readPreference(preferencesRecord.codex),
|
|
92
|
+
opencode: readPreference(preferencesRecord.opencode),
|
|
93
|
+
updatedAt: typeof preferencesRecord.updatedAt === "number" ? preferencesRecord.updatedAt : 0
|
|
94
|
+
};
|
|
95
|
+
const conversations = Array.isArray(record.conversations)
|
|
96
|
+
? record.conversations.map(readConversation).filter((item) => !!item)
|
|
97
|
+
: [];
|
|
98
|
+
const rememberedFoldersRecord = record.rememberedFolders && typeof record.rememberedFolders === "object"
|
|
99
|
+
? record.rememberedFolders
|
|
100
|
+
: {};
|
|
101
|
+
const rememberedFolders = Object.fromEntries(Object.entries(rememberedFoldersRecord)
|
|
102
|
+
.filter(([key, value]) => key.length > 0 &&
|
|
103
|
+
key.length <= 512 &&
|
|
104
|
+
typeof value === "string" &&
|
|
105
|
+
value.length > 0 &&
|
|
106
|
+
value.length <= 4096)
|
|
107
|
+
.slice(-100)
|
|
108
|
+
.map(([key, value]) => [key, value]));
|
|
109
|
+
this.data = { version: 1, preferences, conversations, rememberedFolders };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code !== "ENOENT") {
|
|
113
|
+
const backupPath = `${this.statePath}.corrupt-${Date.now()}`;
|
|
114
|
+
const preserved = await rename(this.statePath, backupPath)
|
|
115
|
+
.then(() => backupPath)
|
|
116
|
+
.catch(() => null);
|
|
117
|
+
console.warn(preserved
|
|
118
|
+
? `Drawsy local AI history was unreadable and was preserved at ${preserved}.`
|
|
119
|
+
: "Drawsy local AI history could not be read; starting empty.", error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async persist() {
|
|
124
|
+
await mkdir(this.stateDirectory, { recursive: true });
|
|
125
|
+
const temporaryPath = `${this.statePath}.${process.pid}.tmp`;
|
|
126
|
+
await writeFile(temporaryPath, `${JSON.stringify(this.data, null, 2)}\n`, {
|
|
127
|
+
encoding: "utf8",
|
|
128
|
+
mode: 0o600
|
|
129
|
+
});
|
|
130
|
+
await rename(temporaryPath, this.statePath);
|
|
131
|
+
}
|
|
132
|
+
async mutate(change) {
|
|
133
|
+
await this.loaded;
|
|
134
|
+
let value;
|
|
135
|
+
const write = this.writeQueue.then(async () => {
|
|
136
|
+
value = await change();
|
|
137
|
+
await this.persist();
|
|
138
|
+
});
|
|
139
|
+
this.writeQueue = write.catch(() => undefined);
|
|
140
|
+
await write;
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
async list(scope, canvasId) {
|
|
144
|
+
await this.loaded;
|
|
145
|
+
return this.data.conversations
|
|
146
|
+
.filter((conversation) => conversation.scope === scope &&
|
|
147
|
+
(scope === "general" || conversation.canvasId === canvasId))
|
|
148
|
+
.sort((left, right) => right.updatedAt - left.updatedAt)
|
|
149
|
+
.map((conversation) => ({ ...conversation }));
|
|
150
|
+
}
|
|
151
|
+
async get(id) {
|
|
152
|
+
await this.loaded;
|
|
153
|
+
const conversation = this.data.conversations.find((item) => item.id === id);
|
|
154
|
+
return conversation ? { ...conversation } : null;
|
|
155
|
+
}
|
|
156
|
+
async upsert(input) {
|
|
157
|
+
return this.mutate(() => {
|
|
158
|
+
const existing = this.data.conversations.find((item) => item.id === input.id);
|
|
159
|
+
const workspaceChanged = !!existing && existing.workspacePath !== input.workspacePath;
|
|
160
|
+
const conversation = existing
|
|
161
|
+
? {
|
|
162
|
+
...existing,
|
|
163
|
+
...input,
|
|
164
|
+
createdAt: existing.createdAt,
|
|
165
|
+
title: existing.title === "New conversation" ? input.title : existing.title,
|
|
166
|
+
messageCount: Math.max(existing.messageCount, input.messageCount),
|
|
167
|
+
codexThreadId: workspaceChanged ? null : existing.codexThreadId,
|
|
168
|
+
openCodeSessionId: workspaceChanged ? null : existing.openCodeSessionId,
|
|
169
|
+
updatedAt: Math.max(existing.updatedAt, input.updatedAt)
|
|
170
|
+
}
|
|
171
|
+
: { ...input, codexThreadId: null, openCodeSessionId: null };
|
|
172
|
+
const index = this.data.conversations.findIndex((item) => item.id === input.id);
|
|
173
|
+
if (index >= 0)
|
|
174
|
+
this.data.conversations[index] = conversation;
|
|
175
|
+
else
|
|
176
|
+
this.data.conversations.push(conversation);
|
|
177
|
+
return { ...conversation };
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async getRememberedFolder(key) {
|
|
181
|
+
await this.loaded;
|
|
182
|
+
return this.data.rememberedFolders[key] || null;
|
|
183
|
+
}
|
|
184
|
+
async rememberFolder(key, folderPath) {
|
|
185
|
+
return this.mutate(() => {
|
|
186
|
+
if (!(key in this.data.rememberedFolders) && Object.keys(this.data.rememberedFolders).length >= 100) {
|
|
187
|
+
const oldestKey = Object.keys(this.data.rememberedFolders)[0];
|
|
188
|
+
if (oldestKey)
|
|
189
|
+
delete this.data.rememberedFolders[oldestKey];
|
|
190
|
+
}
|
|
191
|
+
this.data.rememberedFolders[key] = folderPath;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async forgetRememberedFolder(key) {
|
|
195
|
+
return this.mutate(() => {
|
|
196
|
+
delete this.data.rememberedFolders[key];
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async recordUserMessage(id, message) {
|
|
200
|
+
return this.mutate(() => {
|
|
201
|
+
const conversation = this.data.conversations.find((item) => item.id === id);
|
|
202
|
+
if (!conversation)
|
|
203
|
+
throw new Error("Conversation does not exist locally.");
|
|
204
|
+
const trimmed = message.replace(/\s+/g, " ").trim();
|
|
205
|
+
conversation.updatedAt = Date.now();
|
|
206
|
+
if (conversation.messageCount === 0 && trimmed) {
|
|
207
|
+
conversation.title = trimmed.slice(0, 96);
|
|
208
|
+
}
|
|
209
|
+
conversation.messageCount += 1;
|
|
210
|
+
return { ...conversation };
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async setNativeSession(id, engine, nativeSessionId) {
|
|
214
|
+
return this.mutate(() => {
|
|
215
|
+
const conversation = this.data.conversations.find((item) => item.id === id);
|
|
216
|
+
if (!conversation)
|
|
217
|
+
throw new Error("Conversation does not exist locally.");
|
|
218
|
+
if (engine === "codex")
|
|
219
|
+
conversation.codexThreadId = nativeSessionId;
|
|
220
|
+
else
|
|
221
|
+
conversation.openCodeSessionId = nativeSessionId;
|
|
222
|
+
conversation.updatedAt = Date.now();
|
|
223
|
+
return { ...conversation };
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async getPreferences() {
|
|
227
|
+
await this.loaded;
|
|
228
|
+
return structuredClone(this.data.preferences);
|
|
229
|
+
}
|
|
230
|
+
async setPreferences(input) {
|
|
231
|
+
return this.mutate(() => {
|
|
232
|
+
this.data.preferences = {
|
|
233
|
+
engine: input.engine,
|
|
234
|
+
codex: readPreference(input.codex),
|
|
235
|
+
opencode: readPreference(input.opencode),
|
|
236
|
+
updatedAt: Date.now()
|
|
237
|
+
};
|
|
238
|
+
return structuredClone(this.data.preferences);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
export const resolveDrawsyMcpEntry = (moduleUrl) => {
|
|
4
|
+
const entry = path.resolve(path.dirname(fileURLToPath(moduleUrl)), "mcp.js");
|
|
5
|
+
if (!process.versions.electron)
|
|
6
|
+
return entry;
|
|
7
|
+
return entry.replace(`${path.sep}app.asar${path.sep}`, `${path.sep}app.asar.unpacked${path.sep}`);
|
|
8
|
+
};
|
|
9
|
+
export const drawsyMcpProcess = (entry) => ({
|
|
10
|
+
command: process.execPath,
|
|
11
|
+
args: [entry],
|
|
12
|
+
environment: process.versions.electron
|
|
13
|
+
? { ELECTRON_RUN_AS_NODE: "1" }
|
|
14
|
+
: {}
|
|
15
|
+
});
|