@agnamo/nopilot 0.0.0-stage → 0.1.1
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 +15 -0
- package/NOTICE +11 -0
- package/README.md +40 -2
- package/main.js +2320 -0
- package/manifest.json +9 -0
- package/package.json +48 -4
- package/styles.css +247 -0
package/main.js
ADDED
|
@@ -0,0 +1,2320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __typeError = (msg) => {
|
|
9
|
+
throw TypeError(msg);
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
33
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
34
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
35
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
36
|
+
|
|
37
|
+
// src/main.ts
|
|
38
|
+
var main_exports = {};
|
|
39
|
+
__export(main_exports, {
|
|
40
|
+
default: () => NopilotPlugin
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(main_exports);
|
|
43
|
+
var import_obsidian6 = require("obsidian");
|
|
44
|
+
|
|
45
|
+
// src/agent-runner.ts
|
|
46
|
+
var OUTPUT_LIMIT = 8e3;
|
|
47
|
+
var SHELL_METACHARACTERS = /[&|<>;$`\n\r"']/;
|
|
48
|
+
async function runLocalAgent(options) {
|
|
49
|
+
if (!options.enabled) {
|
|
50
|
+
return failure("Agent is off. Enable it in Nopilot settings.");
|
|
51
|
+
}
|
|
52
|
+
const command = options.command.trim();
|
|
53
|
+
if (command.length === 0) {
|
|
54
|
+
return failure("Agent command is empty.");
|
|
55
|
+
}
|
|
56
|
+
if (isShellLine(options.command) || isShellLine(command)) {
|
|
57
|
+
return failure("Agent command must be one executable, not a shell line.");
|
|
58
|
+
}
|
|
59
|
+
if (options.prompt.trim().length === 0) {
|
|
60
|
+
return failure("Nothing to send to the agent.");
|
|
61
|
+
}
|
|
62
|
+
const cwd = options.cwd.trim();
|
|
63
|
+
if (cwd.length === 0) {
|
|
64
|
+
return failure("Agent working directory is empty.");
|
|
65
|
+
}
|
|
66
|
+
let childProcess;
|
|
67
|
+
try {
|
|
68
|
+
childProcess = await loadChildProcess();
|
|
69
|
+
} catch {
|
|
70
|
+
return failure("Local agent needs the desktop app.");
|
|
71
|
+
}
|
|
72
|
+
return runExecFile(
|
|
73
|
+
childProcess,
|
|
74
|
+
command,
|
|
75
|
+
options.args ?? [],
|
|
76
|
+
options.prompt,
|
|
77
|
+
cwd,
|
|
78
|
+
options.timeoutMs,
|
|
79
|
+
options.sendStdin !== false
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
function isShellLine(command) {
|
|
83
|
+
return SHELL_METACHARACTERS.test(command) || command.includes("..");
|
|
84
|
+
}
|
|
85
|
+
function failure(reason, code = null, stdout = "", stderr = "") {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
code,
|
|
89
|
+
stdout: truncate(stdout),
|
|
90
|
+
stderr: truncate(stderr),
|
|
91
|
+
reason
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function truncate(value) {
|
|
95
|
+
return value.length <= OUTPUT_LIMIT ? value : value.slice(0, OUTPUT_LIMIT);
|
|
96
|
+
}
|
|
97
|
+
function asText(value) {
|
|
98
|
+
if (typeof value === "string") {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
if (value == null) {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
return value.toString("utf8");
|
|
105
|
+
}
|
|
106
|
+
async function loadChildProcess() {
|
|
107
|
+
const moduleName = "child_process";
|
|
108
|
+
const loaded = await loadBuiltin(moduleName);
|
|
109
|
+
return asChildProcessModule(loaded);
|
|
110
|
+
}
|
|
111
|
+
async function loadBuiltin(moduleName) {
|
|
112
|
+
const lexical = readLexicalRequire();
|
|
113
|
+
if (lexical) {
|
|
114
|
+
try {
|
|
115
|
+
return lexical(moduleName);
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const hostRequire = readHostRequire();
|
|
120
|
+
if (hostRequire) {
|
|
121
|
+
try {
|
|
122
|
+
return hostRequire(moduleName);
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return loadWithCreateRequire(moduleName);
|
|
127
|
+
}
|
|
128
|
+
function readLexicalRequire() {
|
|
129
|
+
if (typeof require !== "function") {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return require;
|
|
133
|
+
}
|
|
134
|
+
function readHostRequire() {
|
|
135
|
+
const host = globalThis;
|
|
136
|
+
if (typeof host.require !== "function") {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return host.require;
|
|
140
|
+
}
|
|
141
|
+
async function loadWithCreateRequire(moduleName) {
|
|
142
|
+
const imported = await import(moduleSpecifier());
|
|
143
|
+
if (!isCreateRequireModule(imported)) {
|
|
144
|
+
throw new Error("createRequire missing");
|
|
145
|
+
}
|
|
146
|
+
return imported.createRequire(anchorFile())(moduleName);
|
|
147
|
+
}
|
|
148
|
+
function anchorFile() {
|
|
149
|
+
const cwd = typeof process === "object" && process !== null && typeof process.cwd === "function" ? process.cwd() : ".";
|
|
150
|
+
return `${cwd.split("\\").join("/")}/nopilot-anchor.cjs`;
|
|
151
|
+
}
|
|
152
|
+
function moduleSpecifier() {
|
|
153
|
+
return ["mod", "ule"].join("");
|
|
154
|
+
}
|
|
155
|
+
function isCreateRequireModule(value) {
|
|
156
|
+
if (typeof value !== "object" || value === null || !("createRequire" in value)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const createRequire = value.createRequire;
|
|
160
|
+
return typeof createRequire === "function";
|
|
161
|
+
}
|
|
162
|
+
function asChildProcessModule(value) {
|
|
163
|
+
if (typeof value !== "object" || value === null || !("execFile" in value)) {
|
|
164
|
+
throw new Error("execFile missing");
|
|
165
|
+
}
|
|
166
|
+
const execFile = value.execFile;
|
|
167
|
+
if (typeof execFile !== "function") {
|
|
168
|
+
throw new Error("execFile missing");
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
function runExecFile(childProcess, command, args, prompt, cwd, timeoutMs, sendStdin) {
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
let settled = false;
|
|
175
|
+
const finish = (result) => {
|
|
176
|
+
if (settled) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
settled = true;
|
|
180
|
+
resolve(result);
|
|
181
|
+
};
|
|
182
|
+
let child;
|
|
183
|
+
try {
|
|
184
|
+
child = childProcess.execFile(
|
|
185
|
+
command,
|
|
186
|
+
cleanArgs(args),
|
|
187
|
+
{
|
|
188
|
+
cwd,
|
|
189
|
+
timeout: timeoutMs ?? 12e4,
|
|
190
|
+
windowsHide: true,
|
|
191
|
+
maxBuffer: 1e6,
|
|
192
|
+
shell: false
|
|
193
|
+
},
|
|
194
|
+
(error, stdout, stderr) => {
|
|
195
|
+
finish(resultFromExec(error, asText(stdout), asText(stderr)));
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
finish(
|
|
200
|
+
failure(error instanceof Error ? error.message : "Local agent needs the desktop app.")
|
|
201
|
+
);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const stdin = child.stdin;
|
|
205
|
+
if (stdin == null) {
|
|
206
|
+
stopChild(child);
|
|
207
|
+
finish(failure("Local agent needs the desktop app."));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!sendStdin) {
|
|
211
|
+
try {
|
|
212
|
+
stdin.end();
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
stdin.on("error", (error) => {
|
|
218
|
+
stopChild(child);
|
|
219
|
+
finish(failure(error.message || "Failed to send the prompt."));
|
|
220
|
+
});
|
|
221
|
+
try {
|
|
222
|
+
stdin.write(prompt);
|
|
223
|
+
stdin.end();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
stopChild(child);
|
|
226
|
+
finish(failure(error instanceof Error ? error.message : "Failed to send the prompt."));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function resultFromExec(error, stdout, stderr) {
|
|
231
|
+
if (error == null) {
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
code: 0,
|
|
235
|
+
stdout: truncate(stdout),
|
|
236
|
+
stderr: truncate(stderr)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (error.killed === true || Boolean(error.signal) || typeof error.code !== "number") {
|
|
240
|
+
return failure(error.message, null, stdout, stderr);
|
|
241
|
+
}
|
|
242
|
+
if (error.code === 0) {
|
|
243
|
+
return {
|
|
244
|
+
ok: true,
|
|
245
|
+
code: 0,
|
|
246
|
+
stdout: truncate(stdout),
|
|
247
|
+
stderr: truncate(stderr)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
code: error.code,
|
|
253
|
+
stdout: truncate(stdout),
|
|
254
|
+
stderr: truncate(stderr)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function cleanArgs(args) {
|
|
258
|
+
if (args.length > 24) {
|
|
259
|
+
throw new Error("Agent arguments are not allowed.");
|
|
260
|
+
}
|
|
261
|
+
const cleaned = [];
|
|
262
|
+
for (const arg of args) {
|
|
263
|
+
if (arg.length === 0 || arg.length > 8e3 || arg.includes("\0")) {
|
|
264
|
+
throw new Error("Agent arguments are not allowed.");
|
|
265
|
+
}
|
|
266
|
+
cleaned.push(arg);
|
|
267
|
+
}
|
|
268
|
+
return cleaned;
|
|
269
|
+
}
|
|
270
|
+
function stopChild(child) {
|
|
271
|
+
try {
|
|
272
|
+
child.kill();
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function writeQueryFile(text) {
|
|
277
|
+
const fs2 = asFs(await loadBuiltin("fs"));
|
|
278
|
+
const os = asOs(await loadBuiltin("os"));
|
|
279
|
+
const path2 = asPath(await loadBuiltin("path"));
|
|
280
|
+
const dir = fs2.mkdtempSync(path2.join(os.tmpdir(), "nopilot-packet-"));
|
|
281
|
+
const file = path2.join(dir, "packet.md");
|
|
282
|
+
fs2.writeFileSync(file, text, "utf8");
|
|
283
|
+
return file;
|
|
284
|
+
}
|
|
285
|
+
async function removeQueryFile(filePath) {
|
|
286
|
+
try {
|
|
287
|
+
const fs2 = asFs(await loadBuiltin("fs"));
|
|
288
|
+
const os = asOs(await loadBuiltin("os"));
|
|
289
|
+
const path2 = asPath(await loadBuiltin("path"));
|
|
290
|
+
const root = path2.join(os.tmpdir(), "nopilot-packet-");
|
|
291
|
+
if (!filePath.startsWith(root)) return;
|
|
292
|
+
fs2.rmSync(path2.dirname(filePath), { recursive: true, force: true });
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function asFs(value) {
|
|
297
|
+
if (!isRecord(value) || typeof value.mkdtempSync !== "function" || typeof value.writeFileSync !== "function") {
|
|
298
|
+
throw new Error("Local agent needs the desktop app.");
|
|
299
|
+
}
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
function asOs(value) {
|
|
303
|
+
if (!isRecord(value) || typeof value.tmpdir !== "function") {
|
|
304
|
+
throw new Error("Local agent needs the desktop app.");
|
|
305
|
+
}
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
function asPath(value) {
|
|
309
|
+
if (!isRecord(value) || typeof value.join !== "function" || typeof value.dirname !== "function") {
|
|
310
|
+
throw new Error("Local agent needs the desktop app.");
|
|
311
|
+
}
|
|
312
|
+
return value;
|
|
313
|
+
}
|
|
314
|
+
function isRecord(value) {
|
|
315
|
+
return typeof value === "object" && value !== null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/chat-view.ts
|
|
319
|
+
var import_obsidian2 = require("obsidian");
|
|
320
|
+
|
|
321
|
+
// src/constants.ts
|
|
322
|
+
var VIEW_TYPE_CHAT = "nopilot-chat";
|
|
323
|
+
var DEFAULT_SETTINGS = {
|
|
324
|
+
provider: "openai-compatible",
|
|
325
|
+
baseUrl: "https://api.deepseek.com",
|
|
326
|
+
apiKey: "",
|
|
327
|
+
model: "deepseek-chat",
|
|
328
|
+
systemPrompt: "You are Nopilot, the LuxDev vault assistant. Be concise. When note context is provided, cite the note title. Do not invent notes.",
|
|
329
|
+
conversationsFolder: "nopilot/conversations",
|
|
330
|
+
promptsFolder: "nopilot/prompts",
|
|
331
|
+
skillsFolder: "nopilot/skills",
|
|
332
|
+
memoryFolder: "nopilot/memory",
|
|
333
|
+
contextTurns: 12,
|
|
334
|
+
includeActiveNote: true,
|
|
335
|
+
includeSelection: true,
|
|
336
|
+
temperature: 0.7,
|
|
337
|
+
agentCommand: "opencode",
|
|
338
|
+
agentEnabled: false,
|
|
339
|
+
backend: "byok",
|
|
340
|
+
modules: {
|
|
341
|
+
byok: true,
|
|
342
|
+
hermes: true,
|
|
343
|
+
opencode: true,
|
|
344
|
+
pi: true
|
|
345
|
+
},
|
|
346
|
+
hermesCommand: "hermes",
|
|
347
|
+
opencodeCommand: "opencode",
|
|
348
|
+
piCommand: "pi",
|
|
349
|
+
subagentEnabled: false,
|
|
350
|
+
subagentBackend: "pi",
|
|
351
|
+
maxSubagents: 1,
|
|
352
|
+
includeDirectory: true
|
|
353
|
+
};
|
|
354
|
+
function surfaceLabel(settings) {
|
|
355
|
+
if (settings.backend === "byok") {
|
|
356
|
+
const model = settings.model.trim();
|
|
357
|
+
return model.length > 0 ? model : "byok";
|
|
358
|
+
}
|
|
359
|
+
return settings.backend;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/vault-context.ts
|
|
363
|
+
var import_obsidian = require("obsidian");
|
|
364
|
+
var ACTIVE_NOTE_LIMIT = 12e3;
|
|
365
|
+
var SELECTION_LIMIT = 8e3;
|
|
366
|
+
var MEMORY_LIMIT = 4e3;
|
|
367
|
+
function approximateTokens(text) {
|
|
368
|
+
if (text.length === 0) {
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
return Math.ceil(text.length / 4);
|
|
372
|
+
}
|
|
373
|
+
function truncate2(text, max) {
|
|
374
|
+
if (text.length <= max) return text;
|
|
375
|
+
const marker = "\n\n[truncated]\n";
|
|
376
|
+
const keep = Math.max(0, max - marker.length);
|
|
377
|
+
return `${text.slice(0, keep)}${marker}`;
|
|
378
|
+
}
|
|
379
|
+
function isNonEmpty(value) {
|
|
380
|
+
return value !== null && value.length > 0;
|
|
381
|
+
}
|
|
382
|
+
function vaultPath(folder, name) {
|
|
383
|
+
const joined = `${folder}/${name}`.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
384
|
+
return joined.replace(/^\//, "");
|
|
385
|
+
}
|
|
386
|
+
function renderContextBlock(ctx) {
|
|
387
|
+
const sections = [];
|
|
388
|
+
const title = ctx.activeNoteTitle ?? "";
|
|
389
|
+
const path2 = ctx.activeNotePath ?? "";
|
|
390
|
+
const body = ctx.activeNoteBody ?? "";
|
|
391
|
+
if (title.length > 0 || path2.length > 0 || body.length > 0) {
|
|
392
|
+
sections.push(`Active note: ${title} (${path2})
|
|
393
|
+
---
|
|
394
|
+
${body}
|
|
395
|
+
---`);
|
|
396
|
+
}
|
|
397
|
+
if (isNonEmpty(ctx.selection)) {
|
|
398
|
+
sections.push(`Selection:
|
|
399
|
+
${ctx.selection}
|
|
400
|
+
---`);
|
|
401
|
+
}
|
|
402
|
+
if (isNonEmpty(ctx.memory)) {
|
|
403
|
+
sections.push(`Memory:
|
|
404
|
+
${ctx.memory}`);
|
|
405
|
+
}
|
|
406
|
+
if (sections.length === 0) {
|
|
407
|
+
return "";
|
|
408
|
+
}
|
|
409
|
+
return `[Nopilot context]
|
|
410
|
+
${sections.join("\n")}`;
|
|
411
|
+
}
|
|
412
|
+
async function readVaultContext(app, settings) {
|
|
413
|
+
const ctx = {
|
|
414
|
+
activeNotePath: null,
|
|
415
|
+
activeNoteTitle: null,
|
|
416
|
+
activeNoteBody: null,
|
|
417
|
+
selection: null,
|
|
418
|
+
memory: null
|
|
419
|
+
};
|
|
420
|
+
if (settings.includeActiveNote) {
|
|
421
|
+
try {
|
|
422
|
+
const file = app.workspace.getActiveFile();
|
|
423
|
+
const view = app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
|
|
424
|
+
const editorFile = view?.file;
|
|
425
|
+
if (file && file.extension.toLowerCase() === "md") {
|
|
426
|
+
const raw = view && editorFile?.path === file.path ? view.editor.getValue() : await app.vault.cachedRead(file);
|
|
427
|
+
ctx.activeNotePath = file.path;
|
|
428
|
+
ctx.activeNoteTitle = file.basename;
|
|
429
|
+
ctx.activeNoteBody = truncate2(raw, ACTIVE_NOTE_LIMIT);
|
|
430
|
+
}
|
|
431
|
+
} catch {
|
|
432
|
+
ctx.activeNotePath = null;
|
|
433
|
+
ctx.activeNoteTitle = null;
|
|
434
|
+
ctx.activeNoteBody = null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (settings.includeSelection) {
|
|
438
|
+
try {
|
|
439
|
+
const view = app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
|
|
440
|
+
if (view) {
|
|
441
|
+
const selected = view.editor.getSelection();
|
|
442
|
+
ctx.selection = selected.trim().length > 0 ? truncate2(selected, SELECTION_LIMIT) : null;
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
ctx.selection = null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (settings.memoryFolder) {
|
|
449
|
+
try {
|
|
450
|
+
const path2 = vaultPath(settings.memoryFolder, "memory.md");
|
|
451
|
+
const abstract = app.vault.getAbstractFileByPath(path2);
|
|
452
|
+
if (abstract instanceof import_obsidian.TFile) {
|
|
453
|
+
const raw = await app.vault.cachedRead(abstract);
|
|
454
|
+
ctx.memory = truncate2(raw, MEMORY_LIMIT);
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
ctx.memory = null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return ctx;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/chat-view.ts
|
|
464
|
+
var _messages;
|
|
465
|
+
var NopilotChatView = class extends import_obsidian2.ItemView {
|
|
466
|
+
constructor(leaf, host) {
|
|
467
|
+
super(leaf);
|
|
468
|
+
// A method and a field cannot share this name under strict TS.
|
|
469
|
+
__privateAdd(this, _messages, []);
|
|
470
|
+
this.pending = false;
|
|
471
|
+
this.extraContext = [];
|
|
472
|
+
this.epoch = 0;
|
|
473
|
+
this.host = host;
|
|
474
|
+
}
|
|
475
|
+
getViewType() {
|
|
476
|
+
return VIEW_TYPE_CHAT;
|
|
477
|
+
}
|
|
478
|
+
getDisplayText() {
|
|
479
|
+
return "Nopilot";
|
|
480
|
+
}
|
|
481
|
+
getIcon() {
|
|
482
|
+
return "message-square";
|
|
483
|
+
}
|
|
484
|
+
messages() {
|
|
485
|
+
return __privateGet(this, _messages).slice();
|
|
486
|
+
}
|
|
487
|
+
/** Host save path. Same shallow copy as messages(). */
|
|
488
|
+
getMessages() {
|
|
489
|
+
return this.messages();
|
|
490
|
+
}
|
|
491
|
+
draft() {
|
|
492
|
+
return this.inputEl?.value ?? "";
|
|
493
|
+
}
|
|
494
|
+
isBusy() {
|
|
495
|
+
return this.pending;
|
|
496
|
+
}
|
|
497
|
+
refreshLabel() {
|
|
498
|
+
if (this.modelEl) {
|
|
499
|
+
this.modelEl.textContent = surfaceLabel(this.host.settings);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
newChat() {
|
|
503
|
+
if (this.pending) {
|
|
504
|
+
new import_obsidian2.Notice("Wait for the current reply.");
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
this.epoch += 1;
|
|
508
|
+
__privateSet(this, _messages, []);
|
|
509
|
+
this.extraContext = [];
|
|
510
|
+
this.clearError();
|
|
511
|
+
this.renderLog();
|
|
512
|
+
}
|
|
513
|
+
addContext(label, body) {
|
|
514
|
+
this.extraContext.push({ label, body });
|
|
515
|
+
this.renderLog();
|
|
516
|
+
}
|
|
517
|
+
async sendText(text) {
|
|
518
|
+
const trimmed = text.trim();
|
|
519
|
+
if (trimmed.length === 0 || this.pending) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
this.pending = true;
|
|
523
|
+
if (this.sendBtn) {
|
|
524
|
+
this.sendBtn.disabled = true;
|
|
525
|
+
}
|
|
526
|
+
const epoch = this.epoch;
|
|
527
|
+
try {
|
|
528
|
+
const content = await this.buildUserContent(trimmed);
|
|
529
|
+
if (epoch !== this.epoch) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
__privateGet(this, _messages).push({
|
|
533
|
+
role: "user",
|
|
534
|
+
content,
|
|
535
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
536
|
+
});
|
|
537
|
+
this.extraContext = [];
|
|
538
|
+
if (this.inputEl) {
|
|
539
|
+
this.inputEl.value = "";
|
|
540
|
+
}
|
|
541
|
+
this.clearError();
|
|
542
|
+
this.renderLog();
|
|
543
|
+
const outbound = this.outboundMessages();
|
|
544
|
+
const result = await this.host.complete(outbound);
|
|
545
|
+
if (epoch !== this.epoch) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
__privateGet(this, _messages).push({
|
|
549
|
+
role: "assistant",
|
|
550
|
+
content: result.text,
|
|
551
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
552
|
+
});
|
|
553
|
+
} catch (err) {
|
|
554
|
+
if (epoch !== this.epoch) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const message = thrownMessage(err);
|
|
558
|
+
this.setError(message);
|
|
559
|
+
new import_obsidian2.Notice(message);
|
|
560
|
+
} finally {
|
|
561
|
+
this.pending = false;
|
|
562
|
+
if (this.sendBtn) {
|
|
563
|
+
this.sendBtn.disabled = false;
|
|
564
|
+
}
|
|
565
|
+
if (this.logEl?.isConnected) {
|
|
566
|
+
this.renderLog();
|
|
567
|
+
this.logEl.scrollTop = this.logEl.scrollHeight;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async onOpen() {
|
|
572
|
+
const root = this.contentEl;
|
|
573
|
+
root.empty();
|
|
574
|
+
root.addClass("nopilot-root");
|
|
575
|
+
const header = root.createDiv({ cls: "nopilot-header" });
|
|
576
|
+
header.createEl("span", { text: "Nopilot" });
|
|
577
|
+
this.modelEl = header.createEl("span", {
|
|
578
|
+
cls: "nopilot-model",
|
|
579
|
+
text: surfaceLabel(this.host.settings)
|
|
580
|
+
});
|
|
581
|
+
this.logEl = root.createDiv({ cls: "nopilot-log" });
|
|
582
|
+
this.errorEl = root.createDiv({ cls: "nopilot-error" });
|
|
583
|
+
this.errorEl.hidden = true;
|
|
584
|
+
const composer = root.createDiv({ cls: "nopilot-composer" });
|
|
585
|
+
this.inputEl = composer.createEl("textarea", {
|
|
586
|
+
cls: "nopilot-input",
|
|
587
|
+
attr: { placeholder: "Write to Nopilot", rows: "3" }
|
|
588
|
+
});
|
|
589
|
+
this.sendBtn = composer.createEl("button", {
|
|
590
|
+
cls: "nopilot-send",
|
|
591
|
+
text: "Send",
|
|
592
|
+
attr: { type: "button" }
|
|
593
|
+
});
|
|
594
|
+
this.inputEl.addEventListener("keydown", (evt) => {
|
|
595
|
+
if (evt.key !== "Enter" || evt.shiftKey || evt.isComposing || evt.repeat) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
evt.preventDefault();
|
|
599
|
+
void this.sendText(this.inputEl.value);
|
|
600
|
+
});
|
|
601
|
+
this.sendBtn.addEventListener("click", () => {
|
|
602
|
+
void this.sendText(this.inputEl.value);
|
|
603
|
+
});
|
|
604
|
+
this.renderLog();
|
|
605
|
+
}
|
|
606
|
+
async onClose() {
|
|
607
|
+
this.contentEl.empty();
|
|
608
|
+
}
|
|
609
|
+
async buildUserContent(trimmed) {
|
|
610
|
+
let content = trimmed;
|
|
611
|
+
if (this.host.settings.includeActiveNote || this.host.settings.includeSelection) {
|
|
612
|
+
const ctx = await this.host.readContext();
|
|
613
|
+
const block = renderContextBlock(ctx);
|
|
614
|
+
if (block.trim().length > 0) {
|
|
615
|
+
content = `${block}
|
|
616
|
+
|
|
617
|
+
${content}`;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (this.extraContext.length > 0) {
|
|
621
|
+
const extras = this.extraContext.map((item) => `[${item.label}]
|
|
622
|
+
${item.body}`).join("\n\n");
|
|
623
|
+
content = `${content}
|
|
624
|
+
|
|
625
|
+
${extras}`;
|
|
626
|
+
}
|
|
627
|
+
return content;
|
|
628
|
+
}
|
|
629
|
+
outboundMessages() {
|
|
630
|
+
const nonSystem = __privateGet(this, _messages).filter((message) => message.role !== "system");
|
|
631
|
+
const turns = Math.max(1, this.host.settings.contextTurns);
|
|
632
|
+
const recent = nonSystem.slice(-turns);
|
|
633
|
+
const outbound = [];
|
|
634
|
+
const prompt = this.host.settings.systemPrompt;
|
|
635
|
+
if (prompt.trim().length > 0) {
|
|
636
|
+
outbound.push({
|
|
637
|
+
role: "system",
|
|
638
|
+
content: prompt,
|
|
639
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
outbound.push(...recent);
|
|
643
|
+
return outbound;
|
|
644
|
+
}
|
|
645
|
+
renderLog() {
|
|
646
|
+
if (!this.logEl?.isConnected) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (this.modelEl) {
|
|
650
|
+
this.modelEl.textContent = surfaceLabel(this.host.settings);
|
|
651
|
+
}
|
|
652
|
+
this.logEl.empty();
|
|
653
|
+
for (const extra of this.extraContext) {
|
|
654
|
+
this.logEl.createDiv({ cls: "nopilot-context", text: extra.label });
|
|
655
|
+
}
|
|
656
|
+
const visible = __privateGet(this, _messages).filter((message) => message.role !== "system");
|
|
657
|
+
if (visible.length === 0) {
|
|
658
|
+
this.logEl.createDiv({ cls: "nopilot-empty", text: "No messages yet." });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
for (const message of visible) {
|
|
662
|
+
const row = this.logEl.createDiv({
|
|
663
|
+
cls: message.role === "user" ? "nopilot-msg nopilot-msg-user" : "nopilot-msg nopilot-msg-assistant"
|
|
664
|
+
});
|
|
665
|
+
row.createDiv({ text: message.role === "user" ? "You" : "Nopilot" });
|
|
666
|
+
const body = row.createDiv();
|
|
667
|
+
body.textContent = message.content;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
setError(message) {
|
|
671
|
+
if (!this.errorEl) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
this.errorEl.textContent = message;
|
|
675
|
+
this.errorEl.hidden = message.length === 0;
|
|
676
|
+
}
|
|
677
|
+
clearError() {
|
|
678
|
+
this.setError("");
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
_messages = new WeakMap();
|
|
682
|
+
function thrownMessage(err) {
|
|
683
|
+
if (err instanceof Error && err.message.length > 0) {
|
|
684
|
+
return err.message;
|
|
685
|
+
}
|
|
686
|
+
if (typeof err === "string" && err.length > 0) {
|
|
687
|
+
return err;
|
|
688
|
+
}
|
|
689
|
+
return "Nopilot request failed.";
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/commands.ts
|
|
693
|
+
var import_obsidian3 = require("obsidian");
|
|
694
|
+
function registerNopilotCommands(plugin, actions) {
|
|
695
|
+
add(plugin, "open-chat", "Open chat", actions.openChat);
|
|
696
|
+
add(plugin, "new-chat", "New chat", actions.newChat);
|
|
697
|
+
add(plugin, "save-chat", "Save chat", actions.saveChat);
|
|
698
|
+
add(plugin, "add-active-note", "Add active note", actions.addActiveNote);
|
|
699
|
+
add(plugin, "add-selection", "Add selection", actions.addSelection);
|
|
700
|
+
add(plugin, "summarize-selection", "Summarize selection", actions.summarizeSelection);
|
|
701
|
+
add(plugin, "count-selection", "Count selection", actions.countSelection);
|
|
702
|
+
add(plugin, "run-agent", "Run agent", actions.runAgent);
|
|
703
|
+
add(plugin, "storage-check", "Storage check", actions.checkStorage);
|
|
704
|
+
add(plugin, "open-island-tree", "Open Island tree", actions.openIslandTree);
|
|
705
|
+
add(plugin, "open-island-tree-sidebar", "Open Island tree in sidebar", actions.openIslandTreeSidebar);
|
|
706
|
+
add(plugin, "translate-theme", "Translate current theme settings", actions.translateTheme);
|
|
707
|
+
add(plugin, "translate-all-themes", "Translate all theme settings", actions.translateAllThemes);
|
|
708
|
+
}
|
|
709
|
+
function add(plugin, id, name, action) {
|
|
710
|
+
plugin.addCommand({
|
|
711
|
+
id,
|
|
712
|
+
name,
|
|
713
|
+
callback: () => {
|
|
714
|
+
void action().catch((error) => {
|
|
715
|
+
const message = error instanceof Error && error.message.length > 0 ? error.message : "Nopilot command failed.";
|
|
716
|
+
new import_obsidian3.Notice(message);
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/conversations.ts
|
|
723
|
+
function headingFor(role) {
|
|
724
|
+
switch (role) {
|
|
725
|
+
case "user":
|
|
726
|
+
return "## You";
|
|
727
|
+
case "assistant":
|
|
728
|
+
return "## Nopilot";
|
|
729
|
+
case "system":
|
|
730
|
+
return "## System";
|
|
731
|
+
default: {
|
|
732
|
+
const unreachable = role;
|
|
733
|
+
return unreachable;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function errorMessage(err) {
|
|
738
|
+
if (err instanceof Error) {
|
|
739
|
+
return err.message;
|
|
740
|
+
}
|
|
741
|
+
if (typeof err === "string") {
|
|
742
|
+
return err;
|
|
743
|
+
}
|
|
744
|
+
if (typeof err === "object" && err !== null && "message" in err) {
|
|
745
|
+
const message = err.message;
|
|
746
|
+
if (typeof message === "string") {
|
|
747
|
+
return message;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return "";
|
|
751
|
+
}
|
|
752
|
+
function alreadyExists(err) {
|
|
753
|
+
return errorMessage(err).toLowerCase().includes("already exists");
|
|
754
|
+
}
|
|
755
|
+
function normalizeFolder(folder) {
|
|
756
|
+
const normalized = folder.replace(/\\/g, "/").replace(/\/{2,}/g, "/").replace(/^\/+|\/+$/g, "");
|
|
757
|
+
if (normalized.length === 0 || /^[a-zA-Z]:/.test(normalized)) {
|
|
758
|
+
throw new Error("Folder must be a relative vault path.");
|
|
759
|
+
}
|
|
760
|
+
const parts = normalized.split("/");
|
|
761
|
+
if (parts.some((part) => part.length === 0 || part === "." || part === "..")) {
|
|
762
|
+
throw new Error("Folder must be a relative vault path.");
|
|
763
|
+
}
|
|
764
|
+
return normalized;
|
|
765
|
+
}
|
|
766
|
+
async function ensureFolder(app, folderPath) {
|
|
767
|
+
const parts = folderPath.split("/");
|
|
768
|
+
let current = "";
|
|
769
|
+
for (const part of parts) {
|
|
770
|
+
current = current.length > 0 ? `${current}/${part}` : part;
|
|
771
|
+
try {
|
|
772
|
+
await app.vault.createFolder(current);
|
|
773
|
+
} catch (err) {
|
|
774
|
+
if (!alreadyExists(err)) {
|
|
775
|
+
throw err;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function localStamp(now) {
|
|
781
|
+
const pad = (value, width = 2) => value.toString().padStart(width, "0");
|
|
782
|
+
return `${pad(now.getFullYear(), 4)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
783
|
+
}
|
|
784
|
+
function conversationMarkdown(messages) {
|
|
785
|
+
const saved = (/* @__PURE__ */ new Date()).toISOString();
|
|
786
|
+
const header = `---
|
|
787
|
+
type: nopilot-conversation
|
|
788
|
+
saved: ${saved}
|
|
789
|
+
---`;
|
|
790
|
+
if (messages.length === 0) {
|
|
791
|
+
return `${header}
|
|
792
|
+
`;
|
|
793
|
+
}
|
|
794
|
+
const body = messages.map((message) => `${headingFor(message.role)}
|
|
795
|
+
|
|
796
|
+
${message.content}`).join("\n\n");
|
|
797
|
+
return `${header}
|
|
798
|
+
|
|
799
|
+
${body}
|
|
800
|
+
`;
|
|
801
|
+
}
|
|
802
|
+
async function saveConversation(app, folder, messages) {
|
|
803
|
+
if (messages.length === 0) {
|
|
804
|
+
throw new Error("Nothing to save.");
|
|
805
|
+
}
|
|
806
|
+
const folderPath = normalizeFolder(folder);
|
|
807
|
+
await ensureFolder(app, folderPath);
|
|
808
|
+
const markdown = conversationMarkdown(messages);
|
|
809
|
+
const base = `${folderPath}/`;
|
|
810
|
+
const stamp = localStamp(/* @__PURE__ */ new Date());
|
|
811
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
812
|
+
const suffix = attempt === 0 ? "" : `-${attempt + 1}`;
|
|
813
|
+
const path2 = `${base}Nopilot-${stamp}${suffix}.md`;
|
|
814
|
+
try {
|
|
815
|
+
await app.vault.create(path2, markdown);
|
|
816
|
+
return path2;
|
|
817
|
+
} catch (err) {
|
|
818
|
+
if (!alreadyExists(err) || attempt === 19) {
|
|
819
|
+
throw err;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
throw new Error("Could not save chat.");
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/providers.ts
|
|
827
|
+
var import_obsidian4 = require("obsidian");
|
|
828
|
+
var SLICE_LIMIT = 180;
|
|
829
|
+
var ANTHROPIC_VERSION = "2023-06-01";
|
|
830
|
+
var ANTHROPIC_MAX_TOKENS = 4096;
|
|
831
|
+
async function completeChat(request) {
|
|
832
|
+
const { settings } = request;
|
|
833
|
+
const apiKey = settings.apiKey.trim();
|
|
834
|
+
if (apiKey.length === 0) {
|
|
835
|
+
throw new Error("Nopilot needs an API key. Add one in settings.");
|
|
836
|
+
}
|
|
837
|
+
const model = settings.model.trim();
|
|
838
|
+
if (model.length === 0) {
|
|
839
|
+
throw new Error("Nopilot needs a model name.");
|
|
840
|
+
}
|
|
841
|
+
const messages = toOutbound(request.messages);
|
|
842
|
+
if (messages.length === 0) {
|
|
843
|
+
throw new Error("Nopilot has nothing to send.");
|
|
844
|
+
}
|
|
845
|
+
if (settings.provider === "openai-compatible") {
|
|
846
|
+
return completeOpenAI(settings, apiKey, model, messages);
|
|
847
|
+
}
|
|
848
|
+
if (settings.provider === "anthropic") {
|
|
849
|
+
return completeAnthropic(settings, apiKey, model, messages);
|
|
850
|
+
}
|
|
851
|
+
return unsupportedProvider(settings.provider);
|
|
852
|
+
}
|
|
853
|
+
function toOutbound(messages) {
|
|
854
|
+
const outbound = [];
|
|
855
|
+
for (const message of messages) {
|
|
856
|
+
if (message.content.trim().length === 0) continue;
|
|
857
|
+
outbound.push({ role: message.role, content: message.content });
|
|
858
|
+
}
|
|
859
|
+
return outbound;
|
|
860
|
+
}
|
|
861
|
+
function providerUrl(provider, baseUrl) {
|
|
862
|
+
const base = httpBase(baseUrl);
|
|
863
|
+
if (provider === "anthropic") return anthropicEndpoint(base);
|
|
864
|
+
return openaiEndpoint(base);
|
|
865
|
+
}
|
|
866
|
+
function httpBase(baseUrl) {
|
|
867
|
+
const base = baseUrl.trim();
|
|
868
|
+
if (/\s/.test(base) || !/^https?:\/\//i.test(base)) {
|
|
869
|
+
throw new Error("Provider URL must be an http(s) address.");
|
|
870
|
+
}
|
|
871
|
+
return base.replace(/\/+$/, "");
|
|
872
|
+
}
|
|
873
|
+
function openaiEndpoint(base) {
|
|
874
|
+
if (base.endsWith("/chat/completions")) return base;
|
|
875
|
+
return `${base}/chat/completions`;
|
|
876
|
+
}
|
|
877
|
+
function anthropicEndpoint(base) {
|
|
878
|
+
if (base.endsWith("/v1/messages") || base.endsWith("/messages")) return base;
|
|
879
|
+
if (base.endsWith("/v1")) return `${base}/messages`;
|
|
880
|
+
return `${base}/v1/messages`;
|
|
881
|
+
}
|
|
882
|
+
async function completeOpenAI(settings, apiKey, model, messages) {
|
|
883
|
+
const result = await postJson(
|
|
884
|
+
providerUrl("openai-compatible", settings.baseUrl),
|
|
885
|
+
{
|
|
886
|
+
Authorization: `Bearer ${apiKey}`,
|
|
887
|
+
"Content-Type": "application/json"
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
model,
|
|
891
|
+
messages,
|
|
892
|
+
temperature: settings.temperature,
|
|
893
|
+
stream: false
|
|
894
|
+
},
|
|
895
|
+
apiKey
|
|
896
|
+
);
|
|
897
|
+
const text = openAIText(parseJson(result.text, result.status, apiKey));
|
|
898
|
+
if (text === void 0) {
|
|
899
|
+
throw missingCompletion(result.status, result.text, apiKey);
|
|
900
|
+
}
|
|
901
|
+
return { text, model: settings.model, provider: settings.provider };
|
|
902
|
+
}
|
|
903
|
+
async function completeAnthropic(settings, apiKey, model, messages) {
|
|
904
|
+
const anthropic = toAnthropic(messages);
|
|
905
|
+
if (anthropic.messages.length === 0) {
|
|
906
|
+
throw new Error("Nopilot has nothing to send.");
|
|
907
|
+
}
|
|
908
|
+
const result = await postJson(
|
|
909
|
+
providerUrl("anthropic", settings.baseUrl),
|
|
910
|
+
{
|
|
911
|
+
"x-api-key": apiKey,
|
|
912
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
913
|
+
"content-type": "application/json"
|
|
914
|
+
},
|
|
915
|
+
anthropicBody(settings.model, anthropic),
|
|
916
|
+
apiKey
|
|
917
|
+
);
|
|
918
|
+
const text = anthropicText(parseJson(result.text, result.status, apiKey));
|
|
919
|
+
if (text === void 0) {
|
|
920
|
+
throw missingCompletion(result.status, result.text, apiKey);
|
|
921
|
+
}
|
|
922
|
+
return { text, model: settings.model, provider: settings.provider };
|
|
923
|
+
}
|
|
924
|
+
function anthropicBody(model, anthropic) {
|
|
925
|
+
const body = {
|
|
926
|
+
model,
|
|
927
|
+
max_tokens: ANTHROPIC_MAX_TOKENS,
|
|
928
|
+
messages: anthropic.messages
|
|
929
|
+
};
|
|
930
|
+
if (anthropic.system.length > 0) {
|
|
931
|
+
body.system = anthropic.system;
|
|
932
|
+
}
|
|
933
|
+
return body;
|
|
934
|
+
}
|
|
935
|
+
function toAnthropic(messages) {
|
|
936
|
+
const systemParts = [];
|
|
937
|
+
const rawTurns = [];
|
|
938
|
+
for (const message of messages) {
|
|
939
|
+
if (message.role === "system") {
|
|
940
|
+
systemParts.push(message.content);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
rawTurns.push({ role: message.role, content: message.content });
|
|
944
|
+
}
|
|
945
|
+
const turns = mergeTurns(rawTurns);
|
|
946
|
+
const first = turns[0];
|
|
947
|
+
if (first !== void 0 && first.role === "assistant") {
|
|
948
|
+
turns.unshift({ role: "user", content: "Continue." });
|
|
949
|
+
}
|
|
950
|
+
const system = systemParts.join("\n\n").trim();
|
|
951
|
+
return { system, messages: turns };
|
|
952
|
+
}
|
|
953
|
+
function mergeTurns(turns) {
|
|
954
|
+
const merged = [];
|
|
955
|
+
for (const turn of turns) {
|
|
956
|
+
const last = merged[merged.length - 1];
|
|
957
|
+
if (last && last.role === turn.role) {
|
|
958
|
+
last.content = `${last.content}
|
|
959
|
+
|
|
960
|
+
${turn.content}`;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
merged.push({ role: turn.role, content: turn.content });
|
|
964
|
+
}
|
|
965
|
+
return merged;
|
|
966
|
+
}
|
|
967
|
+
async function postJson(url, headers, body, apiKey) {
|
|
968
|
+
try {
|
|
969
|
+
const response = await (0, import_obsidian4.requestUrl)({
|
|
970
|
+
url,
|
|
971
|
+
method: "POST",
|
|
972
|
+
contentType: "application/json",
|
|
973
|
+
headers,
|
|
974
|
+
body: JSON.stringify(body)
|
|
975
|
+
});
|
|
976
|
+
const text = typeof response.text === "string" ? response.text : "";
|
|
977
|
+
const status = response.status;
|
|
978
|
+
if (status < 200 || status >= 300) {
|
|
979
|
+
throw new Error(
|
|
980
|
+
`Nopilot request failed (${status}): ${clip(text, apiKey)}`
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
return { status, text };
|
|
984
|
+
} catch (error) {
|
|
985
|
+
if (isNopilotError(error)) throw error;
|
|
986
|
+
throw wrapRequestError(error, apiKey);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function openAIText(payload) {
|
|
990
|
+
if (!isRecord2(payload)) return void 0;
|
|
991
|
+
if (!isUnknownArray(payload.choices)) return void 0;
|
|
992
|
+
const first = payload.choices[0];
|
|
993
|
+
if (!isRecord2(first)) return void 0;
|
|
994
|
+
const message = first.message;
|
|
995
|
+
if (!isRecord2(message)) return void 0;
|
|
996
|
+
if (typeof message.content === "string") return message.content;
|
|
997
|
+
if (!Array.isArray(message.content)) return void 0;
|
|
998
|
+
const parts = [];
|
|
999
|
+
for (const block of message.content) {
|
|
1000
|
+
if (typeof block === "string") {
|
|
1001
|
+
parts.push(block);
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
if (!isRecord2(block) || typeof block.text !== "string") continue;
|
|
1005
|
+
parts.push(block.text);
|
|
1006
|
+
}
|
|
1007
|
+
return parts.length > 0 ? parts.join("") : void 0;
|
|
1008
|
+
}
|
|
1009
|
+
function anthropicText(payload) {
|
|
1010
|
+
if (!isRecord2(payload)) return void 0;
|
|
1011
|
+
if (!isUnknownArray(payload.content)) return void 0;
|
|
1012
|
+
const parts = [];
|
|
1013
|
+
for (const block of payload.content) {
|
|
1014
|
+
if (!isRecord2(block)) continue;
|
|
1015
|
+
if (block.type !== "text") continue;
|
|
1016
|
+
if (typeof block.text !== "string") continue;
|
|
1017
|
+
parts.push(block.text);
|
|
1018
|
+
}
|
|
1019
|
+
if (parts.length === 0) return void 0;
|
|
1020
|
+
return parts.join("");
|
|
1021
|
+
}
|
|
1022
|
+
function parseJson(text, status, apiKey) {
|
|
1023
|
+
try {
|
|
1024
|
+
const parsed = JSON.parse(text);
|
|
1025
|
+
return parsed;
|
|
1026
|
+
} catch {
|
|
1027
|
+
throw missingCompletion(status, text, apiKey);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function missingCompletion(status, body, apiKey) {
|
|
1031
|
+
return new Error(
|
|
1032
|
+
`Nopilot completion missing (${status}): ${clip(body, apiKey)}`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
function wrapRequestError(error, apiKey) {
|
|
1036
|
+
const status = readStatus(error);
|
|
1037
|
+
const label = status || "network";
|
|
1038
|
+
return new Error(
|
|
1039
|
+
`Nopilot request failed (${label}): ${clip(readThrownMessage(error), apiKey)}`
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
function readStatus(error) {
|
|
1043
|
+
if (isRecord2(error)) {
|
|
1044
|
+
const status = error.status;
|
|
1045
|
+
if (typeof status === "number" && Number.isFinite(status) && status > 0) {
|
|
1046
|
+
return status;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
1050
|
+
const match = /status (\d{3})\b/.exec(message);
|
|
1051
|
+
if (match === null) return void 0;
|
|
1052
|
+
return Number(match[1]);
|
|
1053
|
+
}
|
|
1054
|
+
function readThrownMessage(error) {
|
|
1055
|
+
if (isRecord2(error) && typeof error.text === "string" && error.text.trim().length > 0) {
|
|
1056
|
+
return error.text;
|
|
1057
|
+
}
|
|
1058
|
+
if (error instanceof Error && error.message.trim().length > 0) {
|
|
1059
|
+
return error.message;
|
|
1060
|
+
}
|
|
1061
|
+
if (typeof error === "string" && error.trim().length > 0) return error;
|
|
1062
|
+
if (isRecord2(error) && typeof error.message === "string" && error.message.trim().length > 0) {
|
|
1063
|
+
return error.message;
|
|
1064
|
+
}
|
|
1065
|
+
return "request failed";
|
|
1066
|
+
}
|
|
1067
|
+
function clip(text, apiKey) {
|
|
1068
|
+
const redacted = redact(text, apiKey).replace(/\s+/g, " ").trim();
|
|
1069
|
+
return redacted.length <= SLICE_LIMIT ? redacted : redacted.slice(0, SLICE_LIMIT);
|
|
1070
|
+
}
|
|
1071
|
+
function redact(text, apiKey) {
|
|
1072
|
+
if (apiKey.length === 0) return text;
|
|
1073
|
+
return text.split(apiKey).join("[redacted]");
|
|
1074
|
+
}
|
|
1075
|
+
function isNopilotError(error) {
|
|
1076
|
+
return error instanceof Error && error.message.startsWith("Nopilot ");
|
|
1077
|
+
}
|
|
1078
|
+
function unsupportedProvider(provider) {
|
|
1079
|
+
throw new Error(`Nopilot does not support provider: ${String(provider)}`);
|
|
1080
|
+
}
|
|
1081
|
+
function isRecord2(value) {
|
|
1082
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1083
|
+
}
|
|
1084
|
+
function isUnknownArray(value) {
|
|
1085
|
+
return Array.isArray(value);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/settings.ts
|
|
1089
|
+
var STRING_KEYS = [
|
|
1090
|
+
"baseUrl",
|
|
1091
|
+
"apiKey",
|
|
1092
|
+
"model",
|
|
1093
|
+
"systemPrompt",
|
|
1094
|
+
"conversationsFolder",
|
|
1095
|
+
"promptsFolder",
|
|
1096
|
+
"skillsFolder",
|
|
1097
|
+
"memoryFolder",
|
|
1098
|
+
"agentCommand",
|
|
1099
|
+
"hermesCommand",
|
|
1100
|
+
"opencodeCommand",
|
|
1101
|
+
"piCommand"
|
|
1102
|
+
];
|
|
1103
|
+
var NUMBER_KEYS = ["contextTurns", "temperature", "maxSubagents"];
|
|
1104
|
+
var BOOLEAN_KEYS = [
|
|
1105
|
+
"includeActiveNote",
|
|
1106
|
+
"includeSelection",
|
|
1107
|
+
"agentEnabled",
|
|
1108
|
+
"subagentEnabled",
|
|
1109
|
+
"includeDirectory"
|
|
1110
|
+
];
|
|
1111
|
+
function isPlainObject(value) {
|
|
1112
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1113
|
+
if (Array.isArray(value)) return false;
|
|
1114
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1115
|
+
return prototype === Object.prototype || prototype === null;
|
|
1116
|
+
}
|
|
1117
|
+
function isProviderKind(value) {
|
|
1118
|
+
return value === "openai-compatible" || value === "anthropic";
|
|
1119
|
+
}
|
|
1120
|
+
function isBackendId(value) {
|
|
1121
|
+
return value === "byok" || value === "hermes" || value === "opencode" || value === "pi";
|
|
1122
|
+
}
|
|
1123
|
+
function isCliBackendId(value) {
|
|
1124
|
+
return value === "hermes" || value === "opencode" || value === "pi";
|
|
1125
|
+
}
|
|
1126
|
+
function readModules(value, base) {
|
|
1127
|
+
const next = { ...base };
|
|
1128
|
+
if (!isPlainObject(value)) return next;
|
|
1129
|
+
const keys = ["byok", "hermes", "opencode", "pi"];
|
|
1130
|
+
for (const key of keys) {
|
|
1131
|
+
if (hasOwn(value, key) && typeof value[key] === "boolean") next[key] = value[key];
|
|
1132
|
+
}
|
|
1133
|
+
return next;
|
|
1134
|
+
}
|
|
1135
|
+
function hasOwn(record, key) {
|
|
1136
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
1137
|
+
}
|
|
1138
|
+
function overlayKnown(next, raw) {
|
|
1139
|
+
if (hasOwn(raw, "provider") && isProviderKind(raw.provider)) {
|
|
1140
|
+
next.provider = raw.provider;
|
|
1141
|
+
}
|
|
1142
|
+
if (hasOwn(raw, "backend") && isBackendId(raw.backend)) {
|
|
1143
|
+
next.backend = raw.backend;
|
|
1144
|
+
}
|
|
1145
|
+
if (hasOwn(raw, "subagentBackend") && isCliBackendId(raw.subagentBackend)) {
|
|
1146
|
+
next.subagentBackend = raw.subagentBackend;
|
|
1147
|
+
}
|
|
1148
|
+
if (hasOwn(raw, "modules")) {
|
|
1149
|
+
next.modules = readModules(raw.modules, next.modules);
|
|
1150
|
+
}
|
|
1151
|
+
for (const key of STRING_KEYS) {
|
|
1152
|
+
if (!hasOwn(raw, key)) continue;
|
|
1153
|
+
const value = raw[key];
|
|
1154
|
+
if (typeof value === "string") next[key] = value;
|
|
1155
|
+
}
|
|
1156
|
+
for (const key of NUMBER_KEYS) {
|
|
1157
|
+
if (!hasOwn(raw, key)) continue;
|
|
1158
|
+
const value = raw[key];
|
|
1159
|
+
if (typeof value === "number" && Number.isFinite(value)) next[key] = value;
|
|
1160
|
+
}
|
|
1161
|
+
for (const key of BOOLEAN_KEYS) {
|
|
1162
|
+
if (!hasOwn(raw, key)) continue;
|
|
1163
|
+
const value = raw[key];
|
|
1164
|
+
if (typeof value === "boolean") next[key] = value;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function mergeSettings(base, raw) {
|
|
1168
|
+
const next = { ...base };
|
|
1169
|
+
try {
|
|
1170
|
+
if (!isPlainObject(raw)) return next;
|
|
1171
|
+
overlayKnown(next, raw);
|
|
1172
|
+
if (next.contextTurns < 1) next.contextTurns = 1;
|
|
1173
|
+
if (next.contextTurns > 50) next.contextTurns = 50;
|
|
1174
|
+
if (next.temperature < 0) next.temperature = 0;
|
|
1175
|
+
if (next.temperature > 2) next.temperature = 2;
|
|
1176
|
+
if (next.maxSubagents < 1) next.maxSubagents = 1;
|
|
1177
|
+
if (next.maxSubagents > 3) next.maxSubagents = 3;
|
|
1178
|
+
if (!next.modules[next.backend]) {
|
|
1179
|
+
const fallback = ["byok", "hermes", "opencode", "pi"].find((id) => next.modules[id]);
|
|
1180
|
+
if (fallback) next.backend = fallback;
|
|
1181
|
+
}
|
|
1182
|
+
if (!next.modules[next.subagentBackend]) next.subagentBackend = "pi";
|
|
1183
|
+
return next;
|
|
1184
|
+
} catch {
|
|
1185
|
+
return { ...base };
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/settings-tab.ts
|
|
1190
|
+
var import_obsidian5 = require("obsidian");
|
|
1191
|
+
|
|
1192
|
+
// src/desk/plan.ts
|
|
1193
|
+
var UNLOADED = "No activation plan loaded.";
|
|
1194
|
+
function unloaded() {
|
|
1195
|
+
return { loaded: false, summary: UNLOADED };
|
|
1196
|
+
}
|
|
1197
|
+
function planSummary(json) {
|
|
1198
|
+
try {
|
|
1199
|
+
if (json === null || typeof json !== "object") return unloaded();
|
|
1200
|
+
const record = json;
|
|
1201
|
+
const target = record.target;
|
|
1202
|
+
if (target !== "lux-assistant" && target !== "nopilot") return unloaded();
|
|
1203
|
+
if (!Array.isArray(record.bindings)) return unloaded();
|
|
1204
|
+
return {
|
|
1205
|
+
loaded: true,
|
|
1206
|
+
summary: `Target ${target} \xB7 ${record.bindings.length} module(s)`
|
|
1207
|
+
};
|
|
1208
|
+
} catch {
|
|
1209
|
+
return unloaded();
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/desk/translate.ts
|
|
1214
|
+
var SETTINGS_BLOCK = /\/\*\s*@settings[\s\S]*?\*\//g;
|
|
1215
|
+
var PHRASE_LIST = [
|
|
1216
|
+
["\u6BDB\u73BB\u7483", "Frosted glass"],
|
|
1217
|
+
["\u5F3A\u8C03\u8272", "Accent color"],
|
|
1218
|
+
["\u4E3B\u9898\u989C\u8272", "Theme color"],
|
|
1219
|
+
["\u80CC\u666F\u989C\u8272", "Background color"],
|
|
1220
|
+
["\u524D\u666F\u989C\u8272", "Foreground color"],
|
|
1221
|
+
["\u4E3B\u8272", "Primary color"],
|
|
1222
|
+
["\u8F85\u8272", "Secondary color"],
|
|
1223
|
+
["\u5B57\u4F53\u5927\u5C0F", "Font size"],
|
|
1224
|
+
["\u5B57\u53F7", "Font size"],
|
|
1225
|
+
["\u5B57\u4F53", "Font"],
|
|
1226
|
+
["\u884C\u9AD8", "Line height"],
|
|
1227
|
+
["\u884C\u8DDD", "Line spacing"],
|
|
1228
|
+
["\u5706\u89D2", "Corner radius"],
|
|
1229
|
+
["\u8FB9\u6846", "Border"],
|
|
1230
|
+
["\u9634\u5F71", "Shadow"],
|
|
1231
|
+
["\u900F\u660E\u5EA6", "Opacity"],
|
|
1232
|
+
["\u6A21\u7CCA", "Blur"],
|
|
1233
|
+
["\u95F4\u8DDD", "Spacing"],
|
|
1234
|
+
["\u5BBD\u5EA6", "Width"],
|
|
1235
|
+
["\u9AD8\u5EA6", "Height"],
|
|
1236
|
+
["\u6807\u9898\u680F", "Title bar"],
|
|
1237
|
+
["\u72B6\u6001\u680F", "Status bar"],
|
|
1238
|
+
["\u529F\u80FD\u533A", "Ribbon"],
|
|
1239
|
+
["\u4FA7\u8FB9\u680F", "Sidebar"],
|
|
1240
|
+
["\u6587\u4EF6\u5217\u8868", "File list"],
|
|
1241
|
+
["\u7F16\u8F91\u5668", "Editor"],
|
|
1242
|
+
["\u6807\u9898", "Heading"],
|
|
1243
|
+
["\u6B63\u6587", "Body text"],
|
|
1244
|
+
["\u80CC\u666F", "Background"],
|
|
1245
|
+
["\u524D\u666F", "Foreground"],
|
|
1246
|
+
["\u989C\u8272", "Color"],
|
|
1247
|
+
["\u8272\u5F69", "Color"],
|
|
1248
|
+
["\u754C\u9762", "Interface"],
|
|
1249
|
+
["\u56FE\u8C31", "Graph"],
|
|
1250
|
+
["\u6807\u7B7E\u9875", "Tab"],
|
|
1251
|
+
["\u6807\u7B7E", "Tag"],
|
|
1252
|
+
["\u94FE\u63A5", "Link"],
|
|
1253
|
+
["\u5F15\u7528\u5757", "Callout"],
|
|
1254
|
+
["\u5F15\u7528", "Quote"],
|
|
1255
|
+
["\u4EE3\u7801\u5757", "Code block"],
|
|
1256
|
+
["\u4EE3\u7801", "Code"],
|
|
1257
|
+
["\u8868\u683C", "Table"],
|
|
1258
|
+
["\u56FE\u7247", "Image"],
|
|
1259
|
+
["\u52A8\u753B", "Animation"],
|
|
1260
|
+
["\u73BB\u7483", "Glass"],
|
|
1261
|
+
["\u9884\u8BBE", "Preset"],
|
|
1262
|
+
["\u81EA\u5B9A\u4E49", "Custom"],
|
|
1263
|
+
["\u9ED8\u8BA4", "Default"],
|
|
1264
|
+
["\u5F53\u524D", "Current"],
|
|
1265
|
+
["\u5168\u90E8", "All"],
|
|
1266
|
+
["\u8BBE\u7F6E", "Settings"],
|
|
1267
|
+
["\u9009\u9879", "Option"],
|
|
1268
|
+
["\u542F\u7528", "Enable"],
|
|
1269
|
+
["\u7981\u7528", "Disable"],
|
|
1270
|
+
["\u6697\u8272", "Dark"],
|
|
1271
|
+
["\u4EAE\u8272", "Light"],
|
|
1272
|
+
["\u6DF1\u8272", "Dark"],
|
|
1273
|
+
["\u6D45\u8272", "Light"],
|
|
1274
|
+
["\u6210\u529F", "Success"],
|
|
1275
|
+
["\u8B66\u544A", "Warning"],
|
|
1276
|
+
["\u9519\u8BEF", "Error"],
|
|
1277
|
+
["\u4FE1\u606F", "Info"],
|
|
1278
|
+
["\u641C\u7D22", "Search"],
|
|
1279
|
+
["\u56FE\u6807", "Icon"],
|
|
1280
|
+
["\u5BF9\u9F50", "Alignment"],
|
|
1281
|
+
["\u5C45\u4E2D", "Center"],
|
|
1282
|
+
["\u7C97\u4F53", "Bold"],
|
|
1283
|
+
["\u659C\u4F53", "Italic"],
|
|
1284
|
+
["\u4E0B\u5212\u7EBF", "Underline"],
|
|
1285
|
+
["\u9875\u7B7E", "Tab"],
|
|
1286
|
+
["\u7B14\u8BB0", "Note"],
|
|
1287
|
+
["\u6587\u4EF6\u5939", "Folder"],
|
|
1288
|
+
["\u57FA\u7840", "Baseline"],
|
|
1289
|
+
["\u4F5C\u66F2", "Composer"],
|
|
1290
|
+
["\u53D1\u5149", "Glow"],
|
|
1291
|
+
["\u7EB8\u611F", "Paper"],
|
|
1292
|
+
["\u4F59\u70EC", "Ember"],
|
|
1293
|
+
["\u5149\u6655", "Lumin"],
|
|
1294
|
+
["\u86CB\u767D\u77F3", "Opal"],
|
|
1295
|
+
["\u5B66\u8005", "Scholar"],
|
|
1296
|
+
["\u65E0", "None"],
|
|
1297
|
+
["\u5F00", "On"],
|
|
1298
|
+
["\u5173", "Off"],
|
|
1299
|
+
["\u5DE6", "Left"],
|
|
1300
|
+
["\u53F3", "Right"],
|
|
1301
|
+
["\u4E0A", "Top"],
|
|
1302
|
+
["\u4E0B", "Bottom"],
|
|
1303
|
+
["\u548C", "and"],
|
|
1304
|
+
["\u4E0E", "and"],
|
|
1305
|
+
["\u6216", "or"],
|
|
1306
|
+
["\u7684", ""],
|
|
1307
|
+
["\u4E3A", "for"],
|
|
1308
|
+
["\u5728", "in"]
|
|
1309
|
+
];
|
|
1310
|
+
var PHRASES = [...PHRASE_LIST].sort((a, b) => b[0].length - a[0].length);
|
|
1311
|
+
function translateFragment(text) {
|
|
1312
|
+
let next = text;
|
|
1313
|
+
for (const [from, to] of PHRASES) {
|
|
1314
|
+
if (next.includes(from)) next = next.split(from).join(to);
|
|
1315
|
+
}
|
|
1316
|
+
return next;
|
|
1317
|
+
}
|
|
1318
|
+
function translateSettingsCss(css) {
|
|
1319
|
+
if (typeof css !== "string" || !css.includes("@settings")) {
|
|
1320
|
+
return { text: typeof css === "string" ? css : "", changed: false };
|
|
1321
|
+
}
|
|
1322
|
+
let changed = false;
|
|
1323
|
+
const text = css.replace(SETTINGS_BLOCK, (block) => {
|
|
1324
|
+
const next = translateFragment(block);
|
|
1325
|
+
if (next !== block) changed = true;
|
|
1326
|
+
return next;
|
|
1327
|
+
});
|
|
1328
|
+
if (!changed) return { text: css, changed: false };
|
|
1329
|
+
return { text, changed: true };
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/desk/vault.ts
|
|
1333
|
+
var PLAN_PATHS = [
|
|
1334
|
+
".obsidian/plugins/nopilot/.kirby/active/nopilot-plan.json",
|
|
1335
|
+
".obsidian/plugins/lux-assistant/.kirby/active/lux-assistant-plan.json"
|
|
1336
|
+
];
|
|
1337
|
+
async function loadPlanSummary(files) {
|
|
1338
|
+
for (const planPath of PLAN_PATHS) {
|
|
1339
|
+
try {
|
|
1340
|
+
if (!await files.exists(planPath)) continue;
|
|
1341
|
+
const summary = planSummary(JSON.parse(await files.read(planPath)));
|
|
1342
|
+
if (summary.loaded) return summary.summary;
|
|
1343
|
+
} catch {
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return "No activation plan loaded.";
|
|
1348
|
+
}
|
|
1349
|
+
async function translateNamedTheme(files, themeName) {
|
|
1350
|
+
const themePath = `.obsidian/themes/${themeName}/theme.css`;
|
|
1351
|
+
if (!await files.exists(themePath)) {
|
|
1352
|
+
return { theme: themeName, missing: true, changed: false };
|
|
1353
|
+
}
|
|
1354
|
+
const original = await files.read(themePath);
|
|
1355
|
+
const translated = translateSettingsCss(original);
|
|
1356
|
+
if (translated.changed) await files.write(themePath, translated.text);
|
|
1357
|
+
return { theme: themeName, missing: false, changed: translated.changed };
|
|
1358
|
+
}
|
|
1359
|
+
async function translateCurrentTheme(files) {
|
|
1360
|
+
let theme = "";
|
|
1361
|
+
try {
|
|
1362
|
+
const appearance = JSON.parse(await files.read(".obsidian/appearance.json"));
|
|
1363
|
+
theme = typeof appearance.cssTheme === "string" ? appearance.cssTheme : "";
|
|
1364
|
+
} catch {
|
|
1365
|
+
theme = "";
|
|
1366
|
+
}
|
|
1367
|
+
if (!theme) return { theme: "", missing: true, changed: false };
|
|
1368
|
+
return translateNamedTheme(files, theme);
|
|
1369
|
+
}
|
|
1370
|
+
async function translateEveryTheme(files) {
|
|
1371
|
+
let names = [];
|
|
1372
|
+
try {
|
|
1373
|
+
const listed = await files.list(".obsidian/themes");
|
|
1374
|
+
names = (listed.folders ?? []).map((folder) => folder.split("/").filter(Boolean).pop() ?? "").filter((name) => name.length > 0).sort();
|
|
1375
|
+
} catch {
|
|
1376
|
+
names = [];
|
|
1377
|
+
}
|
|
1378
|
+
const results = [];
|
|
1379
|
+
for (const name of names) results.push(await translateNamedTheme(files, name));
|
|
1380
|
+
return results;
|
|
1381
|
+
}
|
|
1382
|
+
function themeNotice(result) {
|
|
1383
|
+
if (result.missing) {
|
|
1384
|
+
return result.theme.length > 0 ? `No theme.css found for ${result.theme}.` : "No active theme to translate.";
|
|
1385
|
+
}
|
|
1386
|
+
if (!result.changed) return `${result.theme}: @settings block already translated.`;
|
|
1387
|
+
return `${result.theme}: translated CJK phrases in @settings.`;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/settings-tab.ts
|
|
1391
|
+
var KEY_WARNING = "Your API key is stored in this vault's plugin data (data.json), not in the OS keychain. Do not sync that file if the vault is shared.";
|
|
1392
|
+
var AGENT_WARNING = "CLI backends run only in the desktop app. The executable is one path, shell is off, and the vault packet is a file, not a shell string.";
|
|
1393
|
+
function parseTemperature(value) {
|
|
1394
|
+
const trimmed = value.trim();
|
|
1395
|
+
if (trimmed === "") return null;
|
|
1396
|
+
const parsed = Number(trimmed);
|
|
1397
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 2) return null;
|
|
1398
|
+
return parsed;
|
|
1399
|
+
}
|
|
1400
|
+
function parseContextTurns(value) {
|
|
1401
|
+
const trimmed = value.trim();
|
|
1402
|
+
if (trimmed === "") return null;
|
|
1403
|
+
const parsed = Number(trimmed);
|
|
1404
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1 || parsed > 50) return null;
|
|
1405
|
+
return parsed;
|
|
1406
|
+
}
|
|
1407
|
+
function parsePasses(value) {
|
|
1408
|
+
const trimmed = value.trim();
|
|
1409
|
+
if (trimmed === "") return null;
|
|
1410
|
+
const parsed = Number(trimmed);
|
|
1411
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1 || parsed > 3) return null;
|
|
1412
|
+
return parsed;
|
|
1413
|
+
}
|
|
1414
|
+
var NopilotSettingTab = class extends import_obsidian5.PluginSettingTab {
|
|
1415
|
+
constructor(app, plugin, get, set) {
|
|
1416
|
+
super(app, plugin);
|
|
1417
|
+
this.get = get;
|
|
1418
|
+
this.set = set;
|
|
1419
|
+
}
|
|
1420
|
+
display() {
|
|
1421
|
+
const { containerEl } = this;
|
|
1422
|
+
containerEl.empty();
|
|
1423
|
+
containerEl.addClass("nopilot-settings");
|
|
1424
|
+
const settings = this.get();
|
|
1425
|
+
new import_obsidian5.Setting(containerEl).setName("Nopilot").setHeading();
|
|
1426
|
+
containerEl.createDiv({ cls: "nopilot-key-warning", text: KEY_WARNING });
|
|
1427
|
+
this.backendSection(settings);
|
|
1428
|
+
this.moduleSection(settings);
|
|
1429
|
+
if (settings.modules.byok) this.byokSection(settings);
|
|
1430
|
+
this.workflowSection(settings);
|
|
1431
|
+
this.vaultSection(settings);
|
|
1432
|
+
this.folderSection(settings);
|
|
1433
|
+
this.legacyAgentSection(settings);
|
|
1434
|
+
this.deskSection();
|
|
1435
|
+
}
|
|
1436
|
+
backendSection(settings) {
|
|
1437
|
+
new import_obsidian5.Setting(this.containerEl).setName("Backend").setDesc("Chat sends to this module. CLI backends ignore the HTTP key and run the local agent.").addDropdown((dropdown) => {
|
|
1438
|
+
this.addEnabledBackends(dropdown, settings);
|
|
1439
|
+
dropdown.setValue(settings.backend);
|
|
1440
|
+
dropdown.onChange(async (value) => {
|
|
1441
|
+
if (!this.isEnabledBackend(value, settings)) return;
|
|
1442
|
+
await this.commit("backend", value, true);
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
moduleSection(settings) {
|
|
1447
|
+
const wrap = this.containerEl.createDiv({ cls: "nopilot-module" });
|
|
1448
|
+
new import_obsidian5.Setting(wrap).setName("Modules").setDesc("Turn a module off to remove it from the backend list.").setHeading();
|
|
1449
|
+
this.moduleToggle(wrap, settings, "byok", "Bring your own key", "HTTP module. OpenAI-compatible or Anthropic.");
|
|
1450
|
+
this.moduleToggle(wrap, settings, "hermes", "Hermes", "Quiet oneshot. Does not rewrite your Hermes theme files.");
|
|
1451
|
+
this.moduleToggle(wrap, settings, "opencode", "OpenCode", "opencode run with the vault packet attached.");
|
|
1452
|
+
this.moduleToggle(wrap, settings, "pi", "Pi", "pi --print. No interactive TUI.");
|
|
1453
|
+
if (settings.modules.hermes) this.commandField(wrap, "Hermes command", "hermesCommand", settings.hermesCommand);
|
|
1454
|
+
if (settings.modules.opencode) this.commandField(wrap, "OpenCode command", "opencodeCommand", settings.opencodeCommand);
|
|
1455
|
+
if (settings.modules.pi) this.commandField(wrap, "Pi command", "piCommand", settings.piCommand);
|
|
1456
|
+
}
|
|
1457
|
+
byokSection(settings) {
|
|
1458
|
+
const wrap = this.containerEl.createDiv({ cls: "nopilot-module" });
|
|
1459
|
+
new import_obsidian5.Setting(wrap).setName("Bring your own key").setHeading();
|
|
1460
|
+
new import_obsidian5.Setting(wrap).setName("Provider").addDropdown((dropdown) => {
|
|
1461
|
+
dropdown.addOption("openai-compatible", "OpenAI-compatible");
|
|
1462
|
+
dropdown.addOption("anthropic", "Anthropic");
|
|
1463
|
+
dropdown.setValue(settings.provider);
|
|
1464
|
+
dropdown.onChange(async (value) => {
|
|
1465
|
+
if (value !== "openai-compatible" && value !== "anthropic") return;
|
|
1466
|
+
await this.commit("provider", value);
|
|
1467
|
+
});
|
|
1468
|
+
});
|
|
1469
|
+
this.textField(wrap, "Base URL", "baseUrl", settings.baseUrl);
|
|
1470
|
+
new import_obsidian5.Setting(wrap).setName("API key").addText((text) => {
|
|
1471
|
+
text.inputEl.type = "password";
|
|
1472
|
+
text.inputEl.autocomplete = "off";
|
|
1473
|
+
text.inputEl.spellcheck = false;
|
|
1474
|
+
text.setPlaceholder("Paste key");
|
|
1475
|
+
text.setValue(settings.apiKey);
|
|
1476
|
+
text.onChange(async (value) => {
|
|
1477
|
+
await this.commit("apiKey", value);
|
|
1478
|
+
});
|
|
1479
|
+
});
|
|
1480
|
+
this.textField(wrap, "Model", "model", settings.model);
|
|
1481
|
+
new import_obsidian5.Setting(wrap).setName("Temperature").setDesc("0 to 2. Invalid values are ignored. Anthropic requests omit this.").addText((text) => {
|
|
1482
|
+
text.inputEl.inputMode = "decimal";
|
|
1483
|
+
text.setPlaceholder("0.7");
|
|
1484
|
+
text.setValue(String(settings.temperature));
|
|
1485
|
+
text.onChange(async (value) => {
|
|
1486
|
+
const parsed = parseTemperature(value);
|
|
1487
|
+
if (parsed === null) return;
|
|
1488
|
+
await this.commit("temperature", parsed);
|
|
1489
|
+
});
|
|
1490
|
+
});
|
|
1491
|
+
new import_obsidian5.Setting(wrap).setName("System prompt").addTextArea((text) => {
|
|
1492
|
+
text.setValue(settings.systemPrompt);
|
|
1493
|
+
text.onChange(async (value) => {
|
|
1494
|
+
await this.commit("systemPrompt", value);
|
|
1495
|
+
});
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
workflowSection(settings) {
|
|
1499
|
+
const wrap = this.containerEl.createDiv({ cls: "nopilot-module" });
|
|
1500
|
+
new import_obsidian5.Setting(wrap).setName("Agent workflow").setDesc("A subagent is another CLI pass over the same vault packet plus the prior answer.").setHeading();
|
|
1501
|
+
new import_obsidian5.Setting(wrap).setName("Subagents").addToggle((toggle) => {
|
|
1502
|
+
toggle.setValue(settings.subagentEnabled);
|
|
1503
|
+
toggle.onChange(async (value) => {
|
|
1504
|
+
await this.commit("subagentEnabled", value, true);
|
|
1505
|
+
});
|
|
1506
|
+
});
|
|
1507
|
+
if (!settings.subagentEnabled) return;
|
|
1508
|
+
new import_obsidian5.Setting(wrap).setName("Subagent backend").addDropdown((dropdown) => {
|
|
1509
|
+
this.addCliOptions(dropdown, settings);
|
|
1510
|
+
dropdown.setValue(settings.subagentBackend);
|
|
1511
|
+
dropdown.onChange(async (value) => {
|
|
1512
|
+
if (value !== "hermes" && value !== "opencode" && value !== "pi") return;
|
|
1513
|
+
await this.commit("subagentBackend", value);
|
|
1514
|
+
});
|
|
1515
|
+
});
|
|
1516
|
+
new import_obsidian5.Setting(wrap).setName("Subagent passes").setDesc("1 to 3. Each pass has a different job: missing facts, unsupported claims, then a final cut.").addText((text) => {
|
|
1517
|
+
text.inputEl.type = "number";
|
|
1518
|
+
text.inputEl.min = "1";
|
|
1519
|
+
text.inputEl.max = "3";
|
|
1520
|
+
text.setValue(String(settings.maxSubagents));
|
|
1521
|
+
text.onChange(async (value) => {
|
|
1522
|
+
const parsed = parsePasses(value);
|
|
1523
|
+
if (parsed === null) return;
|
|
1524
|
+
await this.commit("maxSubagents", parsed);
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
vaultSection(settings) {
|
|
1529
|
+
const wrap = this.containerEl.createDiv({ cls: "nopilot-module" });
|
|
1530
|
+
new import_obsidian5.Setting(wrap).setName("Vault context").setHeading();
|
|
1531
|
+
this.toggleField(wrap, "Include active note", "includeActiveNote", settings.includeActiveNote);
|
|
1532
|
+
this.toggleField(wrap, "Include selection", "includeSelection", settings.includeSelection);
|
|
1533
|
+
this.toggleField(wrap, "Include directory list", "includeDirectory", settings.includeDirectory);
|
|
1534
|
+
new import_obsidian5.Setting(wrap).setName("Context turns").setDesc("How many recent chat turns go into the packet. 1 to 50.").addText((text) => {
|
|
1535
|
+
text.inputEl.type = "number";
|
|
1536
|
+
text.inputEl.min = "1";
|
|
1537
|
+
text.inputEl.max = "50";
|
|
1538
|
+
text.setValue(String(settings.contextTurns));
|
|
1539
|
+
text.onChange(async (value) => {
|
|
1540
|
+
const parsed = parseContextTurns(value);
|
|
1541
|
+
if (parsed === null) return;
|
|
1542
|
+
await this.commit("contextTurns", parsed);
|
|
1543
|
+
});
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
folderSection(settings) {
|
|
1547
|
+
const wrap = this.containerEl.createDiv({ cls: "nopilot-module" });
|
|
1548
|
+
new import_obsidian5.Setting(wrap).setName("Vault folders").setDesc("Relative paths only. Drive letters and .. are rejected.").setHeading();
|
|
1549
|
+
this.folderField(wrap, "Conversations folder", "conversationsFolder", settings.conversationsFolder);
|
|
1550
|
+
this.folderField(wrap, "Prompts folder", "promptsFolder", settings.promptsFolder);
|
|
1551
|
+
this.folderField(wrap, "Skills folder", "skillsFolder", settings.skillsFolder);
|
|
1552
|
+
this.folderField(wrap, "Memory folder", "memoryFolder", settings.memoryFolder);
|
|
1553
|
+
}
|
|
1554
|
+
deskSection() {
|
|
1555
|
+
const wrap = this.containerEl.createDiv({ cls: "nopilot-module" });
|
|
1556
|
+
new import_obsidian5.Setting(wrap).setName("Vault desk").setHeading();
|
|
1557
|
+
const status = wrap.createDiv({ text: "Reading plan\u2026" });
|
|
1558
|
+
const files = this.app.vault.adapter;
|
|
1559
|
+
void loadPlanSummary(files).then((summary) => {
|
|
1560
|
+
status.setText(summary);
|
|
1561
|
+
});
|
|
1562
|
+
this.deskButton(wrap, "Storage check", "nopilot:storage-check");
|
|
1563
|
+
this.deskButton(wrap, "Open Island tree", "nopilot:open-island-tree");
|
|
1564
|
+
this.deskButton(wrap, "Translate current theme", "nopilot:translate-theme");
|
|
1565
|
+
}
|
|
1566
|
+
deskButton(parent, name, commandId) {
|
|
1567
|
+
new import_obsidian5.Setting(parent).setName(name).addButton((button) => {
|
|
1568
|
+
button.setButtonText("Run").onClick(() => {
|
|
1569
|
+
const commands = this.app.commands;
|
|
1570
|
+
commands?.executeCommandById?.(commandId);
|
|
1571
|
+
});
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
legacyAgentSection(settings) {
|
|
1575
|
+
const wrap = this.containerEl.createDiv({ cls: "nopilot-module" });
|
|
1576
|
+
wrap.createDiv({ cls: "nopilot-agent-warning", text: AGENT_WARNING });
|
|
1577
|
+
new import_obsidian5.Setting(wrap).setName("Palette agent").setDesc("The Run agent command. Chat already follows the backend above.").addToggle((toggle) => {
|
|
1578
|
+
toggle.setValue(settings.agentEnabled);
|
|
1579
|
+
toggle.onChange(async (value) => {
|
|
1580
|
+
await this.commit("agentEnabled", value, true);
|
|
1581
|
+
});
|
|
1582
|
+
});
|
|
1583
|
+
if (!settings.agentEnabled) return;
|
|
1584
|
+
this.commandField(wrap, "Fallback command", "agentCommand", settings.agentCommand);
|
|
1585
|
+
}
|
|
1586
|
+
moduleToggle(parent, settings, key, name, desc) {
|
|
1587
|
+
new import_obsidian5.Setting(parent).setName(name).setDesc(desc).addToggle((toggle) => {
|
|
1588
|
+
toggle.setValue(settings.modules[key]);
|
|
1589
|
+
toggle.onChange(async (value) => {
|
|
1590
|
+
const modules = { ...this.get().modules, [key]: value };
|
|
1591
|
+
await this.commit("modules", modules, true);
|
|
1592
|
+
});
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
commandField(parent, name, key, value) {
|
|
1596
|
+
new import_obsidian5.Setting(parent).setName(name).setDesc("Executable name or absolute path. No shell syntax.").addText((text) => {
|
|
1597
|
+
text.setValue(value);
|
|
1598
|
+
text.onChange(async (next) => {
|
|
1599
|
+
await this.commit(key, next);
|
|
1600
|
+
});
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
textField(parent, name, key, value) {
|
|
1604
|
+
new import_obsidian5.Setting(parent).setName(name).addText((text) => {
|
|
1605
|
+
text.setValue(value);
|
|
1606
|
+
text.onChange(async (next) => {
|
|
1607
|
+
await this.commit(key, next);
|
|
1608
|
+
});
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
toggleField(parent, name, key, value) {
|
|
1612
|
+
new import_obsidian5.Setting(parent).setName(name).addToggle((toggle) => {
|
|
1613
|
+
toggle.setValue(value);
|
|
1614
|
+
toggle.onChange(async (next) => {
|
|
1615
|
+
await this.commit(key, next);
|
|
1616
|
+
});
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
folderField(parent, name, key, value) {
|
|
1620
|
+
new import_obsidian5.Setting(parent).setName(name).addText((text) => {
|
|
1621
|
+
text.setValue(value);
|
|
1622
|
+
text.onChange(async (next) => {
|
|
1623
|
+
await this.commit(key, next);
|
|
1624
|
+
});
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
addEnabledBackends(dropdown, settings) {
|
|
1628
|
+
if (settings.modules.byok) dropdown.addOption("byok", "Bring your own key");
|
|
1629
|
+
if (settings.modules.hermes) dropdown.addOption("hermes", "Hermes");
|
|
1630
|
+
if (settings.modules.opencode) dropdown.addOption("opencode", "OpenCode");
|
|
1631
|
+
if (settings.modules.pi) dropdown.addOption("pi", "Pi");
|
|
1632
|
+
}
|
|
1633
|
+
addCliOptions(dropdown, settings) {
|
|
1634
|
+
if (settings.modules.hermes) dropdown.addOption("hermes", "Hermes");
|
|
1635
|
+
if (settings.modules.opencode) dropdown.addOption("opencode", "OpenCode");
|
|
1636
|
+
if (settings.modules.pi) dropdown.addOption("pi", "Pi");
|
|
1637
|
+
}
|
|
1638
|
+
isEnabledBackend(value, settings) {
|
|
1639
|
+
if (value === "byok") return settings.modules.byok;
|
|
1640
|
+
if (value === "hermes" || value === "opencode" || value === "pi") return settings.modules[value];
|
|
1641
|
+
return false;
|
|
1642
|
+
}
|
|
1643
|
+
async commit(key, value, redraw = false) {
|
|
1644
|
+
const next = { ...this.get(), [key]: value };
|
|
1645
|
+
await this.set(next);
|
|
1646
|
+
if (redraw) this.display();
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
|
|
1650
|
+
// src/desk/island.ts
|
|
1651
|
+
var ISLAND_OPEN = "island-tree:open";
|
|
1652
|
+
var ISLAND_SIDEBAR = "island-tree:open-in-sidebar";
|
|
1653
|
+
function islandCommand(where) {
|
|
1654
|
+
return where === "sidebar" ? ISLAND_SIDEBAR : ISLAND_OPEN;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/desk/storage.ts
|
|
1658
|
+
function storageStatus() {
|
|
1659
|
+
return [
|
|
1660
|
+
{
|
|
1661
|
+
ok: false,
|
|
1662
|
+
dormant: true,
|
|
1663
|
+
adapter: "S3",
|
|
1664
|
+
message: "S3 adapter is dormant."
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
ok: false,
|
|
1668
|
+
dormant: true,
|
|
1669
|
+
adapter: "Google Drive",
|
|
1670
|
+
message: "Google Drive adapter is dormant."
|
|
1671
|
+
}
|
|
1672
|
+
];
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// src/desk/trigger.ts
|
|
1676
|
+
function isActionName(value) {
|
|
1677
|
+
return value === "storage-check" || value === "open-island-tree" || value === "translate" || value === "translate-all";
|
|
1678
|
+
}
|
|
1679
|
+
function themeRejected(theme) {
|
|
1680
|
+
return theme.length === 0 || theme.includes("/") || theme.includes("\\") || theme.includes("..");
|
|
1681
|
+
}
|
|
1682
|
+
function parseTriggerLine(line) {
|
|
1683
|
+
if (typeof line !== "string") return null;
|
|
1684
|
+
const text = line.replace(/^\uFEFF/, "").trim();
|
|
1685
|
+
if (!text) return null;
|
|
1686
|
+
const parts = text.split("|");
|
|
1687
|
+
if (parts.length > 2) return null;
|
|
1688
|
+
const name = parts[0].trim();
|
|
1689
|
+
if (!isActionName(name)) return null;
|
|
1690
|
+
const hasArg = parts.length === 2;
|
|
1691
|
+
const arg = hasArg ? parts[1].trim() : "";
|
|
1692
|
+
if (name === "storage-check" || name === "translate-all") {
|
|
1693
|
+
if (hasArg) return null;
|
|
1694
|
+
return { action: name };
|
|
1695
|
+
}
|
|
1696
|
+
if (name === "open-island-tree") {
|
|
1697
|
+
if (!hasArg) return { action: name, where: "main" };
|
|
1698
|
+
if (arg === "sidebar") return { action: name, where: "sidebar" };
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
if (!hasArg) return { action: "translate" };
|
|
1702
|
+
if (themeRejected(arg)) return null;
|
|
1703
|
+
return { action: "translate", theme: arg };
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// src/modules/backends.ts
|
|
1707
|
+
var SHELL_LINE = /[&|<>;$`\n\r"']/;
|
|
1708
|
+
function isCliBackend(value) {
|
|
1709
|
+
return value === "hermes" || value === "opencode" || value === "pi";
|
|
1710
|
+
}
|
|
1711
|
+
function assertExecutable(command) {
|
|
1712
|
+
const trimmed = command.trim();
|
|
1713
|
+
if (trimmed.length === 0) {
|
|
1714
|
+
throw new Error("Backend command is empty.");
|
|
1715
|
+
}
|
|
1716
|
+
if (trimmed.length > 260 || SHELL_LINE.test(trimmed) || trimmed.split(/[\\/]/).includes("..")) {
|
|
1717
|
+
throw new Error("Backend command must be one executable, not a shell line.");
|
|
1718
|
+
}
|
|
1719
|
+
return trimmed;
|
|
1720
|
+
}
|
|
1721
|
+
function buildSpawn(backend, options) {
|
|
1722
|
+
const command = assertExecutable(options.command);
|
|
1723
|
+
const cwd = options.cwd.trim();
|
|
1724
|
+
const queryFile = options.queryFile.trim();
|
|
1725
|
+
if (cwd.length === 0 || queryFile.length === 0) {
|
|
1726
|
+
throw new Error("Backend needs a directory and a packet file.");
|
|
1727
|
+
}
|
|
1728
|
+
if (backend === "hermes") {
|
|
1729
|
+
const args = ["chat", "--query-file", queryFile, "--oneshot", "-Q", "--format", "text", "--in", cwd];
|
|
1730
|
+
if (options.bare === true) args.push("--ignore-user-config");
|
|
1731
|
+
return { command, args, cwd, stdin: false };
|
|
1732
|
+
}
|
|
1733
|
+
if (backend === "opencode") {
|
|
1734
|
+
return {
|
|
1735
|
+
command,
|
|
1736
|
+
args: ["run", "-f", queryFile, "Follow the attached Nopilot packet and answer the task."],
|
|
1737
|
+
cwd,
|
|
1738
|
+
stdin: false
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
return {
|
|
1742
|
+
command,
|
|
1743
|
+
args: ["--print", `Read this Nopilot packet and answer the task. Reply with the answer only: ${queryFile}`],
|
|
1744
|
+
cwd,
|
|
1745
|
+
stdin: false
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// src/modules/packet.ts
|
|
1750
|
+
var SUBAGENT_TASKS = [
|
|
1751
|
+
"Check the prior answer against the file list. Add only missing vault facts.",
|
|
1752
|
+
"Remove any claim the packet does not support. Keep the useful remainder.",
|
|
1753
|
+
"Write the final answer the user should keep. Do not repeat the whole trail."
|
|
1754
|
+
];
|
|
1755
|
+
function subagentTask(pass) {
|
|
1756
|
+
const index = Math.max(0, Math.min(SUBAGENT_TASKS.length - 1, pass));
|
|
1757
|
+
return SUBAGENT_TASKS[index] ?? SUBAGENT_TASKS[0];
|
|
1758
|
+
}
|
|
1759
|
+
function renderPacket(input) {
|
|
1760
|
+
const files = input.files.slice(0, 80);
|
|
1761
|
+
const lines = [
|
|
1762
|
+
"# Nopilot packet",
|
|
1763
|
+
`role: ${input.role}`,
|
|
1764
|
+
`backend: ${input.backend}`,
|
|
1765
|
+
`vault: ${input.vault}`,
|
|
1766
|
+
`note: ${input.notePath ?? "none"}`,
|
|
1767
|
+
"",
|
|
1768
|
+
"## Note",
|
|
1769
|
+
input.noteBody ?? "(none)",
|
|
1770
|
+
"",
|
|
1771
|
+
"## Selection",
|
|
1772
|
+
input.selection ?? "(none)",
|
|
1773
|
+
"",
|
|
1774
|
+
"## Memory",
|
|
1775
|
+
input.memory ?? "(none)",
|
|
1776
|
+
"",
|
|
1777
|
+
"## Files",
|
|
1778
|
+
files.length > 0 ? files.map((file) => `- ${file}`).join("\n") : "(none)",
|
|
1779
|
+
"",
|
|
1780
|
+
"## Task",
|
|
1781
|
+
input.task.trim()
|
|
1782
|
+
];
|
|
1783
|
+
if (input.prior && input.prior.trim().length > 0) {
|
|
1784
|
+
lines.push("", "## Prior", input.prior.trim());
|
|
1785
|
+
}
|
|
1786
|
+
return `${lines.join("\n").slice(0, 1e5)}
|
|
1787
|
+
`;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// src/shared/executable.ts
|
|
1791
|
+
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
1792
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
1793
|
+
var SHELL_META = /[&|<>;$`"'\u000a\u000d]/;
|
|
1794
|
+
var MAX_COMMAND_LENGTH = 260;
|
|
1795
|
+
var EXE_LAUNCH = /^"%(?:~dp0|dp0%)[/\\]([^"]+\.exe)"(?:\s+%\*)?\s*$/i;
|
|
1796
|
+
var NODE_LAUNCH = /^node\s+"%(?:~dp0|dp0%)([^"]+\.js)"(?:\s+%\*)?\s*$/i;
|
|
1797
|
+
function assertExecutable2(command) {
|
|
1798
|
+
const trimmed = command.trim();
|
|
1799
|
+
if (trimmed.length === 0) {
|
|
1800
|
+
throw new Error("Command is empty");
|
|
1801
|
+
}
|
|
1802
|
+
if (trimmed.length > MAX_COMMAND_LENGTH) {
|
|
1803
|
+
throw new Error("Command is too long");
|
|
1804
|
+
}
|
|
1805
|
+
if (SHELL_META.test(trimmed)) {
|
|
1806
|
+
throw new Error("Command contains shell metacharacters");
|
|
1807
|
+
}
|
|
1808
|
+
if (trimmed.split(/[\\/]/).some((segment) => segment === "..")) {
|
|
1809
|
+
throw new Error("Command contains a parent path segment");
|
|
1810
|
+
}
|
|
1811
|
+
return trimmed;
|
|
1812
|
+
}
|
|
1813
|
+
function resolveInstalled(command, pathEnv = process.env.PATH ?? "") {
|
|
1814
|
+
return resolveFromPath(command, {
|
|
1815
|
+
pathEnv,
|
|
1816
|
+
readFile(filePath) {
|
|
1817
|
+
try {
|
|
1818
|
+
return import_node_fs.default.readFileSync(filePath, "utf8");
|
|
1819
|
+
} catch {
|
|
1820
|
+
return null;
|
|
1821
|
+
}
|
|
1822
|
+
},
|
|
1823
|
+
isFile(filePath) {
|
|
1824
|
+
try {
|
|
1825
|
+
return import_node_fs.default.statSync(filePath).isFile();
|
|
1826
|
+
} catch {
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
function resolveFromPath(command, options) {
|
|
1833
|
+
const name = assertExecutable2(command);
|
|
1834
|
+
if (/[\\/]/.test(name)) {
|
|
1835
|
+
return resolveDirect(name, options.isFile, options.readFile);
|
|
1836
|
+
}
|
|
1837
|
+
const dirs = options.pathEnv.split(";").map((entry) => entry.trim().replace(/^"|"$/g, "")).filter((entry) => entry.length > 0);
|
|
1838
|
+
for (const ext of [".exe", ".cmd", ".bat"]) {
|
|
1839
|
+
for (const dir of dirs) {
|
|
1840
|
+
const candidate = import_node_path.default.win32.join(dir, name + ext);
|
|
1841
|
+
if (!options.isFile(candidate)) {
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
if (ext === ".exe") {
|
|
1845
|
+
return { file: candidate, argsPrefix: [] };
|
|
1846
|
+
}
|
|
1847
|
+
return parseShim(candidate, options.readFile);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
throw new Error("Executable not found: " + name);
|
|
1851
|
+
}
|
|
1852
|
+
function resolveDirect(file, isFile, readFile) {
|
|
1853
|
+
const lower = file.toLowerCase();
|
|
1854
|
+
if (lower.endsWith(".cmd") || lower.endsWith(".bat") || lower.endsWith(".ps1")) {
|
|
1855
|
+
return parseShim(file, readFile);
|
|
1856
|
+
}
|
|
1857
|
+
if (isFile(file)) {
|
|
1858
|
+
return { file, argsPrefix: [] };
|
|
1859
|
+
}
|
|
1860
|
+
throw new Error("Executable not found: " + file);
|
|
1861
|
+
}
|
|
1862
|
+
function parseShim(shimPath, readFile) {
|
|
1863
|
+
const baseName = import_node_path.default.win32.basename(shimPath);
|
|
1864
|
+
const text = readFile(shimPath);
|
|
1865
|
+
if (text == null) {
|
|
1866
|
+
throw new Error("Executable not found: " + shimPath);
|
|
1867
|
+
}
|
|
1868
|
+
let launch = null;
|
|
1869
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1870
|
+
const trimmed = line.trim();
|
|
1871
|
+
if (trimmed.length === 0 || isBoilerplate(trimmed)) {
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
const parsed = parseLaunchLine(trimmed, shimPath);
|
|
1875
|
+
if (parsed == null || launch != null) {
|
|
1876
|
+
throw new Error("Refusing to run a shell shim: " + baseName);
|
|
1877
|
+
}
|
|
1878
|
+
launch = parsed;
|
|
1879
|
+
}
|
|
1880
|
+
if (launch == null) {
|
|
1881
|
+
throw new Error("Refusing to run a shell shim: " + baseName);
|
|
1882
|
+
}
|
|
1883
|
+
return launch;
|
|
1884
|
+
}
|
|
1885
|
+
function isBoilerplate(line) {
|
|
1886
|
+
return /^@?echo(\s+off)?$/i.test(line) || /^@?goto\s+[A-Za-z0-9_-]+$/i.test(line) || /^:[A-Za-z0-9_-]+$/.test(line) || /^@?setlocal$/i.test(line) || /^@?endlocal$/i.test(line) || /^@?exit\s+\/b(?:\s+\d+)?$/i.test(line) || /^@?call\s+:[A-Za-z0-9_-]+$/i.test(line) || /^@?set\s+dp0=%~dp0$/i.test(line) || /^@?rem(\s|$)/i.test(line) || line.startsWith("::");
|
|
1887
|
+
}
|
|
1888
|
+
function parseLaunchLine(line, shimPath) {
|
|
1889
|
+
const exe = EXE_LAUNCH.exec(line);
|
|
1890
|
+
const exeRelative = exe?.[1];
|
|
1891
|
+
if (exeRelative != null) {
|
|
1892
|
+
return { file: siblingPath(shimPath, exeRelative), argsPrefix: [] };
|
|
1893
|
+
}
|
|
1894
|
+
const node = NODE_LAUNCH.exec(line);
|
|
1895
|
+
const jsRelative = node?.[1];
|
|
1896
|
+
if (jsRelative != null) {
|
|
1897
|
+
return { file: "node", argsPrefix: [siblingPath(shimPath, jsRelative)] };
|
|
1898
|
+
}
|
|
1899
|
+
return null;
|
|
1900
|
+
}
|
|
1901
|
+
function siblingPath(shimPath, relative) {
|
|
1902
|
+
const baseName = import_node_path.default.win32.basename(shimPath);
|
|
1903
|
+
if (/[&|<>;$`"'%\r\n]/.test(relative)) {
|
|
1904
|
+
throw new Error("Refusing to run a shell shim: " + baseName);
|
|
1905
|
+
}
|
|
1906
|
+
const parts = relative.split(/[/\\]+/).filter((part) => part.length > 0);
|
|
1907
|
+
if (parts.length === 0 || parts.some((part) => part === "." || part === ".." || part.includes(":"))) {
|
|
1908
|
+
throw new Error("Refusing to run a shell shim: " + baseName);
|
|
1909
|
+
}
|
|
1910
|
+
const baseDir = import_node_path.default.win32.resolve(import_node_path.default.win32.dirname(shimPath));
|
|
1911
|
+
const resolved = import_node_path.default.win32.resolve(baseDir, ...parts);
|
|
1912
|
+
const rel = import_node_path.default.win32.relative(baseDir, resolved);
|
|
1913
|
+
if (rel.length === 0 || rel.split(/[/\\]+/).includes("..") || import_node_path.default.win32.isAbsolute(rel)) {
|
|
1914
|
+
throw new Error("Refusing to run a shell shim: " + baseName);
|
|
1915
|
+
}
|
|
1916
|
+
return resolved;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// src/main.ts
|
|
1920
|
+
var AGENT_OFF = "Agent is off. Enable it in Nopilot settings.";
|
|
1921
|
+
var AGENT_DESKTOP_ONLY = "Local agent is desktop-only.";
|
|
1922
|
+
var AGENT_FINISHED = "Agent finished.";
|
|
1923
|
+
var AGENT_FAILED = "Agent failed.";
|
|
1924
|
+
var NO_SELECTION = "No selection.";
|
|
1925
|
+
var NO_ACTIVE_NOTE = "No active note.";
|
|
1926
|
+
var NOTICE_LIMIT = 240;
|
|
1927
|
+
var NopilotPlugin = class extends import_obsidian6.Plugin {
|
|
1928
|
+
constructor() {
|
|
1929
|
+
super(...arguments);
|
|
1930
|
+
this.settings = DEFAULT_SETTINGS;
|
|
1931
|
+
}
|
|
1932
|
+
async onload() {
|
|
1933
|
+
this.settings = mergeSettings(DEFAULT_SETTINGS, await this.loadData());
|
|
1934
|
+
this.registerView(VIEW_TYPE_CHAT, (leaf) => new NopilotChatView(leaf, this));
|
|
1935
|
+
this.addRibbonIcon("message-square", "Open Nopilot", () => {
|
|
1936
|
+
void this.activateView().catch((error) => {
|
|
1937
|
+
new import_obsidian6.Notice(clip2(redactKey(errorText(error, "Could not open chat."), this.settings.apiKey)));
|
|
1938
|
+
});
|
|
1939
|
+
});
|
|
1940
|
+
this.addSettingTab(
|
|
1941
|
+
new NopilotSettingTab(this.app, this, () => this.settings, (s) => this.saveSettings(s))
|
|
1942
|
+
);
|
|
1943
|
+
registerNopilotCommands(this, this.commandActions());
|
|
1944
|
+
this.registerObsidianProtocolHandler("nopilot", (params) => {
|
|
1945
|
+
const action = typeof params.action === "string" ? params.action : "";
|
|
1946
|
+
const extra = typeof params.theme === "string" ? params.theme : typeof params.where === "string" ? params.where : "";
|
|
1947
|
+
const line = extra.length > 0 ? `${action}|${extra}` : action;
|
|
1948
|
+
void this.runDesk(parseTriggerLine(line));
|
|
1949
|
+
});
|
|
1950
|
+
this.registerInterval(window.setInterval(() => {
|
|
1951
|
+
void this.pollTrigger();
|
|
1952
|
+
}, 1500));
|
|
1953
|
+
}
|
|
1954
|
+
onunload() {
|
|
1955
|
+
this.app.workspace.detachLeavesOfType(VIEW_TYPE_CHAT);
|
|
1956
|
+
}
|
|
1957
|
+
async saveSettings(next) {
|
|
1958
|
+
if (next) {
|
|
1959
|
+
this.settings = next;
|
|
1960
|
+
}
|
|
1961
|
+
await this.saveData(this.settings);
|
|
1962
|
+
this.existingChat()?.refreshLabel();
|
|
1963
|
+
}
|
|
1964
|
+
async complete(messages) {
|
|
1965
|
+
if (!isCliBackend(this.settings.backend)) {
|
|
1966
|
+
return completeChat({ messages, settings: this.settings });
|
|
1967
|
+
}
|
|
1968
|
+
return this.runWorkflow(messages);
|
|
1969
|
+
}
|
|
1970
|
+
async saveConversation(messages) {
|
|
1971
|
+
return saveConversation(this.app, this.settings.conversationsFolder, messages);
|
|
1972
|
+
}
|
|
1973
|
+
async readContext() {
|
|
1974
|
+
return readVaultContext(this.app, this.settings);
|
|
1975
|
+
}
|
|
1976
|
+
async activateView() {
|
|
1977
|
+
const { workspace } = this.app;
|
|
1978
|
+
let leaf = workspace.getLeavesOfType(VIEW_TYPE_CHAT)[0] ?? null;
|
|
1979
|
+
if (!leaf) {
|
|
1980
|
+
leaf = workspace.getRightLeaf(false);
|
|
1981
|
+
if (!leaf) {
|
|
1982
|
+
return null;
|
|
1983
|
+
}
|
|
1984
|
+
await leaf.setViewState({ type: VIEW_TYPE_CHAT, active: true });
|
|
1985
|
+
}
|
|
1986
|
+
await workspace.revealLeaf(leaf);
|
|
1987
|
+
return leaf.view instanceof NopilotChatView ? leaf.view : null;
|
|
1988
|
+
}
|
|
1989
|
+
async chat() {
|
|
1990
|
+
return this.activateView();
|
|
1991
|
+
}
|
|
1992
|
+
commandActions() {
|
|
1993
|
+
return {
|
|
1994
|
+
openChat: () => this.openChat(),
|
|
1995
|
+
newChat: () => this.newChat(),
|
|
1996
|
+
saveChat: () => this.saveChat(),
|
|
1997
|
+
addActiveNote: () => this.addActiveNote(),
|
|
1998
|
+
addSelection: () => this.addSelection(),
|
|
1999
|
+
summarizeSelection: () => this.summarizeSelection(),
|
|
2000
|
+
countSelection: () => this.countSelection(),
|
|
2001
|
+
runAgent: () => this.runAgent(),
|
|
2002
|
+
checkStorage: () => this.checkStorage(),
|
|
2003
|
+
openIslandTree: () => this.openIsland("main"),
|
|
2004
|
+
openIslandTreeSidebar: () => this.openIsland("sidebar"),
|
|
2005
|
+
translateTheme: () => this.translateOne(),
|
|
2006
|
+
translateAllThemes: () => this.translateAll()
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
async openChat() {
|
|
2010
|
+
await this.activateView();
|
|
2011
|
+
}
|
|
2012
|
+
async newChat() {
|
|
2013
|
+
await (await this.chat())?.newChat();
|
|
2014
|
+
}
|
|
2015
|
+
async saveChat() {
|
|
2016
|
+
const view = this.existingChat();
|
|
2017
|
+
if (!view) {
|
|
2018
|
+
new import_obsidian6.Notice("No chat open.");
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
const messages = this.transcript(view);
|
|
2022
|
+
if (!messages) {
|
|
2023
|
+
new import_obsidian6.Notice("Could not read the chat.");
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
if (messages.length === 0) {
|
|
2027
|
+
new import_obsidian6.Notice("Nothing to save.");
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
try {
|
|
2031
|
+
const path2 = await this.saveConversation(messages);
|
|
2032
|
+
new import_obsidian6.Notice(path2);
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
new import_obsidian6.Notice(errorText(error, "Could not save chat."));
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
async addActiveNote() {
|
|
2038
|
+
const context = await this.readContext();
|
|
2039
|
+
const body = context.activeNoteBody;
|
|
2040
|
+
if (!body) {
|
|
2041
|
+
new import_obsidian6.Notice(NO_ACTIVE_NOTE);
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
const view = await this.chat();
|
|
2045
|
+
if (!view) {
|
|
2046
|
+
new import_obsidian6.Notice("Could not open chat.");
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
const file = this.app.workspace.getActiveFile();
|
|
2050
|
+
const label = context.activeNoteTitle || context.activeNotePath || (file instanceof import_obsidian6.TFile ? file.basename : "Note");
|
|
2051
|
+
await view.addContext(label, body);
|
|
2052
|
+
}
|
|
2053
|
+
async addSelection() {
|
|
2054
|
+
const selection = this.selectionText();
|
|
2055
|
+
if (!selection) {
|
|
2056
|
+
new import_obsidian6.Notice(NO_SELECTION);
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
const view = await this.chat();
|
|
2060
|
+
if (!view) {
|
|
2061
|
+
new import_obsidian6.Notice("Could not open chat.");
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
await view.addContext("Selection", selection);
|
|
2065
|
+
}
|
|
2066
|
+
async summarizeSelection() {
|
|
2067
|
+
const selection = this.selectionText();
|
|
2068
|
+
if (!selection) {
|
|
2069
|
+
new import_obsidian6.Notice(NO_SELECTION);
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
const view = await this.activateView();
|
|
2073
|
+
if (!view) {
|
|
2074
|
+
new import_obsidian6.Notice("Could not open chat.");
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
await view.sendText("Summarize this selection. Keep it short.\n\n" + selection);
|
|
2078
|
+
}
|
|
2079
|
+
async countSelection() {
|
|
2080
|
+
const selection = this.selectionText();
|
|
2081
|
+
if (!selection) {
|
|
2082
|
+
new import_obsidian6.Notice(NO_SELECTION);
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
new import_obsidian6.Notice(`About ${approximateTokens(selection)} tokens (${selection.length} characters).`);
|
|
2086
|
+
}
|
|
2087
|
+
async runAgent() {
|
|
2088
|
+
if (!this.settings.agentEnabled) {
|
|
2089
|
+
new import_obsidian6.Notice(AGENT_OFF);
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
const cwd = this.vaultCwd();
|
|
2093
|
+
if (!cwd) {
|
|
2094
|
+
new import_obsidian6.Notice(AGENT_DESKTOP_ONLY);
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
const prompt = this.agentPrompt();
|
|
2098
|
+
if (!prompt) {
|
|
2099
|
+
new import_obsidian6.Notice("Nothing to send to the agent.");
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
try {
|
|
2103
|
+
const key = this.settings.apiKey;
|
|
2104
|
+
const result = isCliBackend(this.settings.backend) ? await this.spawnBackend(this.settings.backend, cwd, await this.makePacket(this.settings.backend, "primary", prompt)) : await runLocalAgent({
|
|
2105
|
+
command: this.settings.agentCommand,
|
|
2106
|
+
prompt,
|
|
2107
|
+
cwd,
|
|
2108
|
+
enabled: true
|
|
2109
|
+
});
|
|
2110
|
+
if (result.ok) {
|
|
2111
|
+
const preview = redactKey(result.stdout, key).slice(0, NOTICE_LIMIT);
|
|
2112
|
+
new import_obsidian6.Notice(preview.length > 0 ? preview : AGENT_FINISHED);
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
new import_obsidian6.Notice(clip2(redactKey(result.reason || result.stderr || AGENT_FAILED, key)));
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
new import_obsidian6.Notice(clip2(redactKey(errorText(error, AGENT_FAILED), this.settings.apiKey)));
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
transcript(view) {
|
|
2121
|
+
const source = view;
|
|
2122
|
+
if (typeof source.getMessages === "function") {
|
|
2123
|
+
return source.getMessages().slice();
|
|
2124
|
+
}
|
|
2125
|
+
if (Array.isArray(source.messages)) {
|
|
2126
|
+
return source.messages.slice();
|
|
2127
|
+
}
|
|
2128
|
+
return null;
|
|
2129
|
+
}
|
|
2130
|
+
existingChat() {
|
|
2131
|
+
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_CHAT);
|
|
2132
|
+
for (const leaf of leaves) {
|
|
2133
|
+
if (leaf.view instanceof NopilotChatView) {
|
|
2134
|
+
return leaf.view;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return null;
|
|
2138
|
+
}
|
|
2139
|
+
selectionText() {
|
|
2140
|
+
const view = this.app.workspace.getActiveViewOfType(import_obsidian6.MarkdownView);
|
|
2141
|
+
if (!view) {
|
|
2142
|
+
return null;
|
|
2143
|
+
}
|
|
2144
|
+
const text = view.editor.getSelection();
|
|
2145
|
+
return text.length > 0 ? text : null;
|
|
2146
|
+
}
|
|
2147
|
+
agentPrompt() {
|
|
2148
|
+
const draft = this.existingChat()?.draft().trim() ?? "";
|
|
2149
|
+
return draft.length > 0 ? draft : null;
|
|
2150
|
+
}
|
|
2151
|
+
async runWorkflow(messages) {
|
|
2152
|
+
const backend = this.settings.backend;
|
|
2153
|
+
if (!isCliBackend(backend)) {
|
|
2154
|
+
return completeChat({ messages, settings: this.settings });
|
|
2155
|
+
}
|
|
2156
|
+
if (!this.settings.modules[backend]) {
|
|
2157
|
+
throw new Error("That backend module is off.");
|
|
2158
|
+
}
|
|
2159
|
+
const cwd = this.vaultCwd();
|
|
2160
|
+
if (!cwd) {
|
|
2161
|
+
throw new Error(AGENT_DESKTOP_ONLY);
|
|
2162
|
+
}
|
|
2163
|
+
const task = messages.filter((message) => message.role !== "system").map((message) => message.content).join("\n\n");
|
|
2164
|
+
const packet = await this.makePacket(backend, "primary", task);
|
|
2165
|
+
const primary = await this.spawnBackend(backend, cwd, packet);
|
|
2166
|
+
if (!primary.ok) {
|
|
2167
|
+
throw new Error(redactKey(primary.reason || primary.stderr || AGENT_FAILED, this.settings.apiKey));
|
|
2168
|
+
}
|
|
2169
|
+
let text = redactKey(primary.stdout.trim() || AGENT_FINISHED, this.settings.apiKey);
|
|
2170
|
+
if (this.settings.subagentEnabled && this.settings.modules[this.settings.subagentBackend]) {
|
|
2171
|
+
const passes = Math.max(1, Math.min(3, this.settings.maxSubagents));
|
|
2172
|
+
for (let pass = 0; pass < passes; pass += 1) {
|
|
2173
|
+
const brief = await this.makePacket(this.settings.subagentBackend, "subagent", subagentTask(pass), text);
|
|
2174
|
+
const result = await this.spawnBackend(this.settings.subagentBackend, cwd, brief);
|
|
2175
|
+
const piece = result.ok ? result.stdout.trim() || AGENT_FINISHED : result.reason || result.stderr || "Subagent failed.";
|
|
2176
|
+
text = `${text}
|
|
2177
|
+
|
|
2178
|
+
--- subagent ${pass + 1} (${this.settings.subagentBackend}) ---
|
|
2179
|
+
${redactKey(piece, this.settings.apiKey)}`;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return { text, model: backend, provider: this.settings.provider };
|
|
2183
|
+
}
|
|
2184
|
+
async makePacket(backend, role, task, prior) {
|
|
2185
|
+
const context = await this.readContext();
|
|
2186
|
+
const files = this.settings.includeDirectory ? this.markdownPaths() : [];
|
|
2187
|
+
return renderPacket({
|
|
2188
|
+
role,
|
|
2189
|
+
backend,
|
|
2190
|
+
vault: this.vaultCwd() ?? "vault",
|
|
2191
|
+
notePath: context.activeNotePath,
|
|
2192
|
+
noteTitle: context.activeNoteTitle,
|
|
2193
|
+
noteBody: context.activeNoteBody,
|
|
2194
|
+
selection: context.selection,
|
|
2195
|
+
memory: context.memory,
|
|
2196
|
+
files,
|
|
2197
|
+
task,
|
|
2198
|
+
prior
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
markdownPaths() {
|
|
2202
|
+
try {
|
|
2203
|
+
return this.app.vault.getMarkdownFiles().map((file) => file.path).sort().slice(0, 80);
|
|
2204
|
+
} catch {
|
|
2205
|
+
return [];
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
commandFor(backend) {
|
|
2209
|
+
if (backend === "hermes") return this.settings.hermesCommand;
|
|
2210
|
+
if (backend === "opencode") return this.settings.opencodeCommand;
|
|
2211
|
+
return this.settings.piCommand;
|
|
2212
|
+
}
|
|
2213
|
+
async spawnBackend(backend, cwd, packet) {
|
|
2214
|
+
const file = await writeQueryFile(packet);
|
|
2215
|
+
try {
|
|
2216
|
+
const plan = buildSpawn(backend, { command: this.commandFor(backend), cwd, queryFile: file });
|
|
2217
|
+
const resolved = resolveInstalled(plan.command);
|
|
2218
|
+
return await runLocalAgent({
|
|
2219
|
+
command: resolved.file,
|
|
2220
|
+
args: [...resolved.argsPrefix, ...plan.args],
|
|
2221
|
+
prompt: packet,
|
|
2222
|
+
cwd,
|
|
2223
|
+
enabled: true,
|
|
2224
|
+
sendStdin: plan.stdin
|
|
2225
|
+
});
|
|
2226
|
+
} finally {
|
|
2227
|
+
await removeQueryFile(file);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
async checkStorage() {
|
|
2231
|
+
const lines = storageStatus().map((item) => item.message);
|
|
2232
|
+
new import_obsidian6.Notice(lines.join(" "));
|
|
2233
|
+
}
|
|
2234
|
+
async openIsland(where) {
|
|
2235
|
+
const host = this.app;
|
|
2236
|
+
if (host.plugins?.enabledPlugins?.has?.("island-tree") !== true) {
|
|
2237
|
+
new import_obsidian6.Notice("Enable the Island tree plugin, then open the dashboard again.");
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
const commandId = islandCommand(where);
|
|
2241
|
+
const commands = host.commands ?? {};
|
|
2242
|
+
if (commands.executeCommandById?.(commandId) === false) {
|
|
2243
|
+
new import_obsidian6.Notice(`Island tree did not provide ${commandId}.`);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
async translateOne() {
|
|
2247
|
+
const result = await translateCurrentTheme(this.vaultFiles());
|
|
2248
|
+
new import_obsidian6.Notice(themeNotice(result));
|
|
2249
|
+
}
|
|
2250
|
+
async translateAll() {
|
|
2251
|
+
const results = await translateEveryTheme(this.vaultFiles());
|
|
2252
|
+
const changed = results.filter((item) => item.changed).length;
|
|
2253
|
+
new import_obsidian6.Notice(`Theme audit finished. ${changed} of ${results.length} themes changed.`);
|
|
2254
|
+
}
|
|
2255
|
+
async runDesk(action) {
|
|
2256
|
+
if (!action) {
|
|
2257
|
+
new import_obsidian6.Notice("Nopilot does not recognize that action.");
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
if (action.action === "storage-check") return this.checkStorage();
|
|
2261
|
+
if (action.action === "open-island-tree") return this.openIsland(action.where);
|
|
2262
|
+
if (action.action === "translate-all") return this.translateAll();
|
|
2263
|
+
if (action.theme) {
|
|
2264
|
+
new import_obsidian6.Notice(themeNotice(await translateNamedTheme(this.vaultFiles(), action.theme)));
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
return this.translateOne();
|
|
2268
|
+
}
|
|
2269
|
+
async pollTrigger() {
|
|
2270
|
+
const files = this.vaultFiles();
|
|
2271
|
+
const trigger = ".obsidian/plugins/nopilot/.trigger";
|
|
2272
|
+
try {
|
|
2273
|
+
if (!await files.exists(trigger)) return;
|
|
2274
|
+
const raw = await files.read(trigger);
|
|
2275
|
+
const line = raw.replace(/^\uFEFF/, "").trim();
|
|
2276
|
+
if (!line) return;
|
|
2277
|
+
await files.write(trigger, "");
|
|
2278
|
+
await this.runDesk(parseTriggerLine(line));
|
|
2279
|
+
} catch {
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
vaultFiles() {
|
|
2284
|
+
const adapter = this.app.vault.adapter;
|
|
2285
|
+
return adapter;
|
|
2286
|
+
}
|
|
2287
|
+
vaultCwd() {
|
|
2288
|
+
try {
|
|
2289
|
+
const adapter = this.app.vault.adapter;
|
|
2290
|
+
if (!("getBasePath" in adapter)) {
|
|
2291
|
+
return null;
|
|
2292
|
+
}
|
|
2293
|
+
const getBasePath = adapter.getBasePath;
|
|
2294
|
+
if (typeof getBasePath !== "function") {
|
|
2295
|
+
return null;
|
|
2296
|
+
}
|
|
2297
|
+
const cwd = getBasePath.call(adapter);
|
|
2298
|
+
return typeof cwd === "string" && cwd.length > 0 ? cwd : null;
|
|
2299
|
+
} catch {
|
|
2300
|
+
return null;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
function clip2(text) {
|
|
2305
|
+
const trimmed = text.trim();
|
|
2306
|
+
if (trimmed.length === 0) {
|
|
2307
|
+
return AGENT_FAILED;
|
|
2308
|
+
}
|
|
2309
|
+
return trimmed.slice(0, NOTICE_LIMIT);
|
|
2310
|
+
}
|
|
2311
|
+
function errorText(error, fallback) {
|
|
2312
|
+
if (error instanceof Error && error.message.length > 0) {
|
|
2313
|
+
return error.message;
|
|
2314
|
+
}
|
|
2315
|
+
return fallback;
|
|
2316
|
+
}
|
|
2317
|
+
function redactKey(text, apiKey) {
|
|
2318
|
+
if (apiKey.length === 0 || !text.includes(apiKey)) return text;
|
|
2319
|
+
return text.split(apiKey).join("[redacted]");
|
|
2320
|
+
}
|