@jstn-sdk/ma 0.1.12 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex/agents/Architect.toml +1 -2
- package/.codex/agents/Auditor.toml +1 -2
- package/.codex/agents/Builder.toml +1 -2
- package/.codex/agents/Flow.toml +1 -2
- package/.codex/agents/Sage.toml +1 -2
- package/.codex/agents/Vibe.toml +1 -2
- package/.codex/hooks.json +24 -13
- package/.codex/prompts/enforcement.md +4 -0
- package/.codex/prompts/onboarding.md +6 -1
- package/.codex/prompts/release-rules.md +0 -0
- package/.codex/prompts/skill-contract.md +4 -0
- package/COVERAGE.md +211 -0
- package/DEMO.md +274 -0
- package/LICENSE +0 -0
- package/README.md +132 -656
- package/bin/ma.js +748 -55
- package/data/clone-data.ledger.json +41 -0
- package/data/clone-data.proof.json +50 -0
- package/data/clone-data.rvf +37 -0
- package/docs/README.md +10 -1
- package/docs/autonomous-tasks.md +46 -0
- package/docs/codex-integration.md +16 -0
- package/docs/getting-started.md +62 -22
- package/docs/mcp-setup.md +64 -0
- package/docs/qa/release-issue-gates-0.14.0.json +644 -0
- package/docs/qa/release-readiness-0.1.5.md +1 -1
- package/docs/qa/{release-readiness-0.1.12.md → release-readiness-0.14.0.md} +25 -9
- package/docs/quality.md +24 -0
- package/docs/reference/native-security-playbooks.md +6 -0
- package/docs/reference/native-source-selection.md +6 -0
- package/docs/release-spec.md +39 -10
- package/docs/skills.md +14 -1
- package/index.js +295 -2
- package/mcp/collections.json +23 -6
- package/mcp/fallback.json +0 -0
- package/mcp/local/code-intel.js +179 -41
- package/mcp/local/context.js +180 -0
- package/mcp/local/memory.js +17 -6
- package/mcp/local/quality.js +39 -0
- package/mcp/local/state.js +7 -3
- package/mcp/local/team-run.js +56 -4
- package/mcp/local-capabilities.json +9 -0
- package/mcp/servers.json +24 -0
- package/package.json +29 -4
- package/plugins/meta-architect/.app.json +1 -1
- package/plugins/meta-architect/.codex-plugin/plugin.json +1 -1
- package/plugins/meta-architect/.mcp.json +1 -1
- package/plugins/meta-architect/README.md +8 -1
- package/plugins/meta-architect/obsidian/main.js +534 -0
- package/plugins/meta-architect/obsidian/manifest.json +9 -0
- package/plugins/meta-architect/obsidian/styles.css +7 -0
- package/plugins/meta-architect/skills/arch/SKILL.md +2 -0
- package/plugins/meta-architect/skills/build/SKILL.md +3 -0
- package/plugins/meta-architect/skills/flow/SKILL.md +2 -0
- package/plugins/meta-architect/skills/sage/SKILL.md +2 -0
- package/plugins/meta-architect/skills/sage/references/source-selection.md +8 -0
- package/plugins/meta-architect/skills/vet/SKILL.md +2 -0
- package/plugins/meta-architect/skills/vet/references/security-playbooks.md +7 -0
- package/plugins/meta-architect/skills/vibe/SKILL.md +2 -0
- package/schemas/autonomous-task-queue.schema.json +56 -0
- package/schemas/handoff-packet.schema.json +36 -0
- package/schemas/setup-receipt.schema.json +28 -0
- package/schemas/skill-frontmatter.schema.json +20 -0
- package/schemas/support-bundle.schema.json +37 -0
- package/schemas/task-contract.schema.json +31 -0
- package/scripts/active-autonomy-hook.mjs +226 -0
- package/scripts/build-linux-packages.mjs +7 -3
- package/scripts/cleanup-test-fixtures.sh +55 -0
- package/scripts/context-hydration-hook.mjs +89 -0
- package/scripts/demo-smoke.js +285 -0
- package/scripts/doctor.js +27 -12
- package/scripts/install.sh +57 -0
- package/scripts/install.sh.sha256 +1 -0
- package/scripts/linux-package-smoke.mjs +2 -2
- package/scripts/package-size-check.mjs +26 -0
- package/scripts/plugin-sync.js +83 -17
- package/scripts/postinstall.js +17 -10
- package/scripts/prepack.js +9 -0
- package/scripts/ralph/prompt.md +35 -0
- package/scripts/release-sync.js +72 -7
- package/scripts/release-verify.js +260 -2
- package/scripts/setup-npmrc.js +48 -23
- package/scripts/skills-install.js +9 -2
- package/scripts/skills-manifest.js +2 -21
- package/scripts/skills-pack.js +2 -2
- package/scripts/skills-validate.js +5 -55
- package/skills/arch/SKILL.md +2 -0
- package/skills/build/SKILL.md +3 -0
- package/skills/flow/SKILL.md +2 -0
- package/skills/index.json +0 -0
- package/skills/sage/SKILL.md +2 -0
- package/skills/sage/references/source-selection.md +8 -0
- package/skills/vet/SKILL.md +2 -0
- package/skills/vet/references/security-playbooks.md +7 -0
- package/skills/vibe/SKILL.md +2 -0
- package/sprint/00-idea.md +0 -0
- package/sprint/01-architecture.md +0 -0
- package/sprint/02-oss-evidence.md +0 -0
- package/sprint/03-logic.md +0 -0
- package/sprint/04-security.md +0 -0
- package/sprint/05-dx-ux.md +0 -0
- package/sprint/06-build-plan.md +0 -0
- package/sprint/07-release.md +0 -0
- package/src/agents.js +279 -0
- package/src/bootstrap.js +268 -62
- package/src/build-gate.js +2 -2
- package/src/codex-app-server.js +291 -0
- package/src/decision-log.js +0 -0
- package/src/fs-utils.js +175 -4
- package/src/launcher.js +28 -21
- package/src/mcp-config.js +9 -1
- package/src/mcp-live-client.js +422 -3
- package/src/paths.js +10 -1
- package/src/policy.js +3 -3
- package/src/prelaunch.js +189 -0
- package/src/quality/ai-quality-orchestrator.js +328 -0
- package/src/release-issue-gates.js +252 -0
- package/src/release-operations.js +62 -0
- package/src/release-state.js +0 -0
- package/src/runtime/active-autonomy-core.js +208 -0
- package/src/runtime/agent-compat.js +31 -0
- package/src/runtime/alignment-sentinel.js +165 -0
- package/src/runtime/architect-review.js +326 -0
- package/src/runtime/autonomous-tasks.js +408 -0
- package/src/runtime/build-readiness.js +28 -4
- package/src/runtime/code-graph-rehearse.js +113 -0
- package/src/runtime/codeburn-core.js +112 -0
- package/src/runtime/context-authority.js +40 -0
- package/src/runtime/context-economy-core.js +316 -0
- package/src/runtime/continuity-graph.js +201 -0
- package/src/runtime/continuity-notes.js +71 -26
- package/src/runtime/core-source-ingest.js +379 -0
- package/src/runtime/detached-provider.js +142 -0
- package/src/runtime/doctor-report.js +18 -0
- package/src/runtime/environment-awareness-core.js +460 -0
- package/src/runtime/exposure-catalog.js +276 -0
- package/src/runtime/graphify-core.js +167 -0
- package/src/runtime/guidance-stack.js +9 -2
- package/src/runtime/handoff-packets.js +92 -0
- package/src/runtime/headroom-core.js +104 -0
- package/src/runtime/helper-orchestration-core.js +307 -0
- package/src/runtime/hook-profiles.js +20 -0
- package/src/runtime/learning-loop-core.js +344 -0
- package/src/runtime/live-agent-verification.js +122 -0
- package/src/runtime/maestro-events.js +18 -0
- package/src/runtime/maestro-manager.js +19 -0
- package/src/runtime/maestro-output.js +58 -0
- package/src/runtime/maestro-state.js +125 -0
- package/src/runtime/managed-markdown.js +29 -0
- package/src/runtime/mcp-authority.js +147 -0
- package/src/runtime/mcp-policy.js +192 -0
- package/src/runtime/obsidian-integration-core.js +1089 -0
- package/src/runtime/obsidian-plugin-bridge.js +1045 -0
- package/src/runtime/pi-maestro-core.js +89 -0
- package/src/runtime/preferences.js +79 -0
- package/src/runtime/project-context.js +636 -0
- package/src/runtime/prompt-strategy-core.js +230 -0
- package/src/runtime/quorum-review.js +90 -0
- package/src/runtime/ralph-execution-core.js +217 -0
- package/src/runtime/redaction-gateway.js +373 -0
- package/src/runtime/runtime-state.js +935 -2
- package/src/runtime/schema-migrations.js +162 -0
- package/src/runtime/semantic-recording-core.js +147 -0
- package/src/runtime/signal-hooks.js +28 -0
- package/src/runtime/skills-registry-export.js +754 -0
- package/src/runtime/task-contracts.js +83 -0
- package/src/runtime/universal-plugin-broker-core.js +898 -0
- package/src/runtime/workspace-intelligence-runtime.js +674 -0
- package/src/runtime/workspace-virtualizer.js +102 -0
- package/src/runtime-artifacts.js +225 -62
- package/src/setup-lifecycle.js +359 -0
- package/src/skill-frontmatter.js +78 -0
- package/src/skill-installer.js +226 -71
- package/src/skills.js +1166 -96
- package/src/state-sync.js +0 -0
- package/src/test-fixtures.js +543 -0
- package/src/tui/grid.js +67 -0
- package/src/tui/status-grid.js +23 -0
- package/support-bundle.json +114 -0
- package/templates/AGENTS.md +6 -2
- package/templates/catalog-manifest.json +0 -0
- package/templates/model-instructions/core.md +0 -0
- package/templates/model-instructions/release.md +0 -0
- package/templates/model-instructions/security.md +0 -0
- package/templates/quality/ai-quality-rules.yml +37 -0
- package/docs/assets/image/Screenshot(1).png +0 -0
- package/docs/assets/image/Screenshot(2).png +0 -0
- package/docs/assets/image/Screenshot(3).png +0 -0
- package/docs/assets/image/Screenshot(4).png +0 -0
- package/docs/assets/image/Screenshot(5).png +0 -0
- package/docs/assets/image/Screenshot(6).png +0 -0
- package/docs/assets/image/Screenshot(7).png +0 -0
- package/docs/assets/image/Screenshot(8).png +0 -0
- package/docs/assets/image/Screenshot(9).png +0 -0
- package/docs/assets/meta-architect-logo.png +0 -0
- package/docs/assets/meta-architect-logo.svg +0 -8
- package/docs/onboarding.md +0 -65
- package/docs/skills-publishing.md +0 -255
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
export const obsidianPluginId = "meta-architect";
|
|
7
|
+
export const obsidianPluginName = "Meta-Architect";
|
|
8
|
+
export const obsidianPluginSchemaVersion = "0.1.0";
|
|
9
|
+
export const obsidianPluginQueuePath = "Meta-Architect/Plugin Requests/request-queue.json";
|
|
10
|
+
export const obsidianPluginActiveContextPath =
|
|
11
|
+
"Meta-Architect/Plugin Context/Active Note Context.md";
|
|
12
|
+
export const obsidianPluginAttachmentDir = "Meta-Architect/Attachments";
|
|
13
|
+
const defaultAttachmentMaxBytes = 1_048_576;
|
|
14
|
+
const defaultAttachmentContentTypes = new Set([
|
|
15
|
+
"application/octet-stream",
|
|
16
|
+
"application/json",
|
|
17
|
+
"text/plain",
|
|
18
|
+
"text/markdown",
|
|
19
|
+
"image/png",
|
|
20
|
+
"image/jpeg",
|
|
21
|
+
"image/webp",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const forbiddenMaTargets = [".ma/release.json", ".ma/decisions.json", ".ma/plans/", ".ma/specs/"];
|
|
25
|
+
const protocolMaxPayloadBytes = 64 * 1024;
|
|
26
|
+
const protocolActions = new Set(["queue", "capture", "open"]);
|
|
27
|
+
const protocolFields = new Set([
|
|
28
|
+
"action",
|
|
29
|
+
"open",
|
|
30
|
+
"path",
|
|
31
|
+
"note_path",
|
|
32
|
+
"from",
|
|
33
|
+
"to",
|
|
34
|
+
"content",
|
|
35
|
+
"data",
|
|
36
|
+
"content_type",
|
|
37
|
+
"overwrite",
|
|
38
|
+
"id",
|
|
39
|
+
"authority",
|
|
40
|
+
"records_as",
|
|
41
|
+
"protocol_token",
|
|
42
|
+
"protocol_token_hash",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const pluginCapabilityNames = [
|
|
46
|
+
"in_app_plugin_surface",
|
|
47
|
+
"active_note_selection_context",
|
|
48
|
+
"metadata_cache_deep_graph",
|
|
49
|
+
"frontmatter_authority_layer",
|
|
50
|
+
"link_safe_rename_move",
|
|
51
|
+
"protocol_handler",
|
|
52
|
+
"event_watchers",
|
|
53
|
+
"canvas_visual_workspace_context",
|
|
54
|
+
"attachments_binary_resources",
|
|
55
|
+
"plugin_request_queue",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export function createDefaultObsidianPluginBridgeManifest() {
|
|
59
|
+
return {
|
|
60
|
+
schemaVersion: obsidianPluginSchemaVersion,
|
|
61
|
+
plugin_id: obsidianPluginId,
|
|
62
|
+
plugin_name: obsidianPluginName,
|
|
63
|
+
records_as: "vault_context",
|
|
64
|
+
build_evidence: false,
|
|
65
|
+
semantic_role: "brain_context",
|
|
66
|
+
canonical_queue_path: obsidianPluginQueuePath,
|
|
67
|
+
active_context_note_path: obsidianPluginActiveContextPath,
|
|
68
|
+
attachment_directory: obsidianPluginAttachmentDir,
|
|
69
|
+
capabilities: pluginCapabilityNames.map((capability) => ({
|
|
70
|
+
capability,
|
|
71
|
+
enabled_by_default: true,
|
|
72
|
+
records_as: "vault_context",
|
|
73
|
+
build_evidence: false,
|
|
74
|
+
})),
|
|
75
|
+
authority_boundary: {
|
|
76
|
+
authoritative_changes_return_through: "$maestro_or_owning_lane",
|
|
77
|
+
must_not_mutate: [...forbiddenMaTargets],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function installObsidianPlugin({
|
|
83
|
+
vaultPath,
|
|
84
|
+
pluginId = obsidianPluginId,
|
|
85
|
+
packageRoot = getPackageRoot(),
|
|
86
|
+
enable = true,
|
|
87
|
+
} = {}) {
|
|
88
|
+
if (!vaultPath || typeof vaultPath !== "string") {
|
|
89
|
+
throw new Error("Obsidian plugin install requires vaultPath");
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
typeof pluginId !== "string" ||
|
|
93
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(pluginId) ||
|
|
94
|
+
path.isAbsolute(pluginId)
|
|
95
|
+
) {
|
|
96
|
+
throw new Error(`Invalid Obsidian plugin id: ${pluginId}`);
|
|
97
|
+
}
|
|
98
|
+
const resolvedVaultPath = path.resolve(vaultPath);
|
|
99
|
+
const vaultStat = await fs.stat(resolvedVaultPath);
|
|
100
|
+
if (!vaultStat.isDirectory()) {
|
|
101
|
+
throw new Error(`Obsidian vault path is not a directory: ${resolvedVaultPath}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const sourceDir = path.join(packageRoot, "plugins", "meta-architect", "obsidian");
|
|
105
|
+
const targetDir = path.join(resolvedVaultPath, ".obsidian", "plugins", pluginId);
|
|
106
|
+
const relativeTarget = path.relative(resolvedVaultPath, path.resolve(targetDir));
|
|
107
|
+
if (relativeTarget.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTarget)) {
|
|
108
|
+
throw new Error("Obsidian plugin target must remain inside the vault");
|
|
109
|
+
}
|
|
110
|
+
await assertNoSymlinkComponents(resolvedVaultPath, targetDir);
|
|
111
|
+
await assertNoSymlinkComponents(
|
|
112
|
+
resolvedVaultPath,
|
|
113
|
+
path.join(resolvedVaultPath, ".obsidian", "community-plugins.json"),
|
|
114
|
+
);
|
|
115
|
+
const sourceMainPath = path.join(sourceDir, "main.js");
|
|
116
|
+
const sourceManifestPath = path.join(sourceDir, "manifest.json");
|
|
117
|
+
const configPath = path.join(resolvedVaultPath, ".obsidian", "community-plugins.json");
|
|
118
|
+
const originalConfig = await readOptionalFile(configPath);
|
|
119
|
+
if (enable) parseCommunityPlugins(originalConfig);
|
|
120
|
+
const stagingDir = `${targetDir}.staging-${Date.now()}`;
|
|
121
|
+
const backupDir = `${targetDir}.backup-${Date.now()}`;
|
|
122
|
+
let hadTarget = false;
|
|
123
|
+
let installed = false;
|
|
124
|
+
try {
|
|
125
|
+
await fs.mkdir(path.dirname(targetDir), { recursive: true });
|
|
126
|
+
await fs.cp(sourceDir, stagingDir, { recursive: true, force: true });
|
|
127
|
+
|
|
128
|
+
const manifest = JSON.parse(await fs.readFile(path.join(stagingDir, "manifest.json"), "utf8"));
|
|
129
|
+
if (manifest.id !== pluginId) {
|
|
130
|
+
throw new Error(`Installed Obsidian plugin manifest id mismatch: ${manifest.id}`);
|
|
131
|
+
}
|
|
132
|
+
const sourceHash = await hashFile(sourceMainPath);
|
|
133
|
+
const installedHash = await hashFile(path.join(stagingDir, "main.js"));
|
|
134
|
+
const sourceManifestHash = await hashFile(sourceManifestPath);
|
|
135
|
+
const installedManifestHash = await hashFile(path.join(stagingDir, "manifest.json"));
|
|
136
|
+
if (sourceHash !== installedHash || sourceManifestHash !== installedManifestHash) {
|
|
137
|
+
throw new Error("Installed Obsidian plugin integrity mismatch");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
hadTarget = await pathExists(targetDir);
|
|
141
|
+
if (hadTarget) await fs.rename(targetDir, backupDir);
|
|
142
|
+
await fs.rename(stagingDir, targetDir);
|
|
143
|
+
installed = true;
|
|
144
|
+
const enabledPlugins = enable
|
|
145
|
+
? await enableObsidianCommunityPlugin({ vaultPath: resolvedVaultPath, pluginId })
|
|
146
|
+
: null;
|
|
147
|
+
|
|
148
|
+
const installedFiles = await listPluginFiles(targetDir);
|
|
149
|
+
await fs.rm(backupDir, { recursive: true, force: true });
|
|
150
|
+
return {
|
|
151
|
+
record_type: "obsidian_plugin_install",
|
|
152
|
+
records_as: "vault_context",
|
|
153
|
+
build_evidence: false,
|
|
154
|
+
enabled: enable,
|
|
155
|
+
community_plugins: enabledPlugins,
|
|
156
|
+
plugin_id: pluginId,
|
|
157
|
+
vault_path: resolvedVaultPath,
|
|
158
|
+
plugin_path: targetDir,
|
|
159
|
+
source_hash: sourceHash,
|
|
160
|
+
installed_hash: installedHash,
|
|
161
|
+
source_manifest_hash: sourceManifestHash,
|
|
162
|
+
installed_manifest_hash: installedManifestHash,
|
|
163
|
+
installed_files: installedFiles,
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
await fs.rm(stagingDir, { recursive: true, force: true });
|
|
167
|
+
if (installed) await fs.rm(targetDir, { recursive: true, force: true });
|
|
168
|
+
if (hadTarget) await fs.rename(backupDir, targetDir);
|
|
169
|
+
if (originalConfig === null) await fs.rm(configPath, { force: true });
|
|
170
|
+
else await fs.writeFile(configPath, originalConfig, "utf8");
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function registerMetaArchitectObsidianPluginRuntime({
|
|
176
|
+
plugin,
|
|
177
|
+
app = plugin?.app,
|
|
178
|
+
Notice,
|
|
179
|
+
Modal,
|
|
180
|
+
PluginSettingTab,
|
|
181
|
+
Setting,
|
|
182
|
+
MarkdownView,
|
|
183
|
+
settings = {},
|
|
184
|
+
} = {}) {
|
|
185
|
+
if (!plugin || !app) {
|
|
186
|
+
throw new Error("Obsidian plugin runtime registration requires plugin and app");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const statusBar = plugin.addStatusBarItem?.();
|
|
190
|
+
statusBar?.setText?.("MA: vault context");
|
|
191
|
+
|
|
192
|
+
plugin.addRibbonIcon?.("network", "Send current note/selection to Meta-Architect", async () => {
|
|
193
|
+
await captureActiveNoteToVault({ app, Notice, MarkdownView, settings });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
plugin.addCommand?.({
|
|
197
|
+
id: "ma-capture-active-note",
|
|
198
|
+
name: "Send current note/selection to MA",
|
|
199
|
+
callback: async () => {
|
|
200
|
+
await captureActiveNoteToVault({ app, Notice, MarkdownView, settings });
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
plugin.addCommand?.({
|
|
205
|
+
id: "ma-drain-request-queue",
|
|
206
|
+
name: "Drain MA request queue",
|
|
207
|
+
callback: async () => {
|
|
208
|
+
const result = await drainQueueFromVault({ app, MarkdownView, settings });
|
|
209
|
+
if (Notice) {
|
|
210
|
+
new Notice(
|
|
211
|
+
`MA queue drained: ${result.processed.length} processed, ${result.refused.length} refused`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
plugin.addCommand?.({
|
|
218
|
+
id: "ma-open-map",
|
|
219
|
+
name: "Open MA map of content",
|
|
220
|
+
callback: async () => {
|
|
221
|
+
const file = app.vault?.getAbstractFileByPath?.("Meta-Architect/Map of Content.md");
|
|
222
|
+
if (file) await app.workspace?.getLeaf?.(true)?.openFile?.(file);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (PluginSettingTab && Setting && app) {
|
|
227
|
+
plugin.addSettingTab?.(createMetaArchitectSettingTab(app, plugin, PluginSettingTab, Setting));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const protocol = registerObsidianProtocolHandlers({ plugin, app, MarkdownView, settings });
|
|
231
|
+
const watchers = registerObsidianEventWatchers({
|
|
232
|
+
plugin,
|
|
233
|
+
app,
|
|
234
|
+
onEvent: async () => {
|
|
235
|
+
await drainQueueFromVault({ app, MarkdownView, settings }).catch(() => {});
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
record_type: "obsidian_plugin_runtime_registration",
|
|
241
|
+
records_as: "vault_context",
|
|
242
|
+
build_evidence: false,
|
|
243
|
+
ribbon: true,
|
|
244
|
+
status_bar: Boolean(statusBar),
|
|
245
|
+
commands: ["ma-capture-active-note", "ma-drain-request-queue", "ma-open-map"],
|
|
246
|
+
settings_tab: Boolean(PluginSettingTab && Setting),
|
|
247
|
+
protocol,
|
|
248
|
+
watchers,
|
|
249
|
+
modal_available: Boolean(Modal),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function createObsidianSelectionContext({ app, MarkdownView } = {}) {
|
|
254
|
+
const file = app?.workspace?.getActiveFile?.() ?? null;
|
|
255
|
+
const view = app?.workspace?.getActiveViewOfType?.(MarkdownView) ?? null;
|
|
256
|
+
const leaf = app?.workspace?.activeLeaf ?? null;
|
|
257
|
+
const editor = view?.editor ?? leaf?.view?.editor ?? null;
|
|
258
|
+
const selection = safeCall(() => editor?.getSelection?.()) ?? "";
|
|
259
|
+
const cursor = safeCall(() => editor?.getCursor?.()) ?? null;
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
record_type: "obsidian_active_note_context",
|
|
263
|
+
records_as: "vault_context",
|
|
264
|
+
build_evidence: false,
|
|
265
|
+
captured_at: new Date().toISOString(),
|
|
266
|
+
active_note_path: file?.path ?? null,
|
|
267
|
+
active_note_basename: file?.basename ?? basenameWithoutExtension(file?.path),
|
|
268
|
+
selected_text: selection,
|
|
269
|
+
selected_text_char_count: selection.length,
|
|
270
|
+
cursor,
|
|
271
|
+
pane_state: safeCall(() => leaf?.getViewState?.()) ?? null,
|
|
272
|
+
view_type: view?.getViewType?.() ?? leaf?.view?.getViewType?.() ?? null,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function createObsidianMetadataGraph({ app, files } = {}) {
|
|
277
|
+
const metadataCache = app?.metadataCache;
|
|
278
|
+
const vault = app?.vault;
|
|
279
|
+
const candidatePaths = files?.map((file) => file.path ?? file).filter(Boolean);
|
|
280
|
+
const cachedPaths = candidatePaths ?? metadataCache?.getCachedFiles?.() ?? [];
|
|
281
|
+
const nodes = [];
|
|
282
|
+
|
|
283
|
+
for (const filePath of cachedPaths) {
|
|
284
|
+
const file = typeof filePath === "string" ? vault?.getAbstractFileByPath?.(filePath) : filePath;
|
|
285
|
+
const resolvedPath = file?.path ?? filePath;
|
|
286
|
+
if (!resolvedPath || typeof resolvedPath !== "string" || !resolvedPath.endsWith(".md")) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const cache = metadataCache?.getFileCache?.(file ?? { path: resolvedPath }) ?? {};
|
|
290
|
+
nodes.push({
|
|
291
|
+
path: resolvedPath,
|
|
292
|
+
records_as: "vault_context",
|
|
293
|
+
build_evidence: false,
|
|
294
|
+
frontmatter: cache.frontmatter ?? {},
|
|
295
|
+
headings: cache.headings ?? [],
|
|
296
|
+
links: cache.links ?? [],
|
|
297
|
+
embeds: cache.embeds ?? [],
|
|
298
|
+
tags: cache.tags ?? [],
|
|
299
|
+
blocks: cache.blocks ?? {},
|
|
300
|
+
sections: cache.sections ?? [],
|
|
301
|
+
resolved_links: metadataCache?.resolvedLinks?.[resolvedPath] ?? {},
|
|
302
|
+
unresolved_links: metadataCache?.unresolvedLinks?.[resolvedPath] ?? {},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
record_type: "obsidian_metadata_cache_graph",
|
|
308
|
+
records_as: "vault_context",
|
|
309
|
+
build_evidence: false,
|
|
310
|
+
indexed_at: new Date().toISOString(),
|
|
311
|
+
note_count: nodes.length,
|
|
312
|
+
nodes,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export async function applyObsidianFrontmatterAuthority({
|
|
317
|
+
app,
|
|
318
|
+
file,
|
|
319
|
+
project = "Meta-Architect",
|
|
320
|
+
capabilities = pluginCapabilityNames,
|
|
321
|
+
sourceWorkspace = null,
|
|
322
|
+
} = {}) {
|
|
323
|
+
if (!app?.fileManager?.processFrontMatter) {
|
|
324
|
+
throw new Error("Obsidian FileManager.processFrontMatter is required");
|
|
325
|
+
}
|
|
326
|
+
if (!file?.path) {
|
|
327
|
+
throw new Error("Obsidian frontmatter authority requires a file");
|
|
328
|
+
}
|
|
329
|
+
await app.fileManager.processFrontMatter(file, (frontmatter) => {
|
|
330
|
+
frontmatter.ma_records_as = "vault_context";
|
|
331
|
+
frontmatter.ma_project = project;
|
|
332
|
+
frontmatter.ma_capabilities = [...new Set(capabilities)];
|
|
333
|
+
if (sourceWorkspace) frontmatter.ma_source_workspace = sourceWorkspace;
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
record_type: "obsidian_frontmatter_authority",
|
|
337
|
+
records_as: "vault_context",
|
|
338
|
+
build_evidence: false,
|
|
339
|
+
note_path: file.path,
|
|
340
|
+
ma_project: project,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function renameObsidianFileSafely({ app, file, newPath } = {}) {
|
|
345
|
+
assertSafeVaultPath(file?.path);
|
|
346
|
+
assertSafeVaultPath(newPath);
|
|
347
|
+
if (!app?.fileManager?.renameFile) {
|
|
348
|
+
throw new Error("Obsidian FileManager.renameFile is required for link-safe rename/move");
|
|
349
|
+
}
|
|
350
|
+
if (!file?.path) {
|
|
351
|
+
throw new Error("Obsidian link-safe rename requires a file");
|
|
352
|
+
}
|
|
353
|
+
await app.fileManager.renameFile(file, newPath);
|
|
354
|
+
return {
|
|
355
|
+
record_type: "obsidian_link_safe_rename",
|
|
356
|
+
records_as: "vault_context",
|
|
357
|
+
build_evidence: false,
|
|
358
|
+
from: file.path,
|
|
359
|
+
to: newPath,
|
|
360
|
+
method: "FileManager.renameFile",
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function generateObsidianMarkdownLink({ app, file, sourcePath = "", subpath, alias } = {}) {
|
|
365
|
+
if (!app?.fileManager?.generateMarkdownLink) {
|
|
366
|
+
throw new Error("Obsidian FileManager.generateMarkdownLink is required");
|
|
367
|
+
}
|
|
368
|
+
const link = app.fileManager.generateMarkdownLink(file, sourcePath, subpath, alias);
|
|
369
|
+
return {
|
|
370
|
+
record_type: "obsidian_generated_markdown_link",
|
|
371
|
+
records_as: "vault_context",
|
|
372
|
+
build_evidence: false,
|
|
373
|
+
link,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function registerObsidianProtocolHandlers({
|
|
378
|
+
plugin,
|
|
379
|
+
app,
|
|
380
|
+
MarkdownView,
|
|
381
|
+
settings = {},
|
|
382
|
+
} = {}) {
|
|
383
|
+
if (!plugin?.registerObsidianProtocolHandler) {
|
|
384
|
+
return { registered: false, handler: "ma", reason: "protocol_handler_unavailable" };
|
|
385
|
+
}
|
|
386
|
+
plugin.registerObsidianProtocolHandler("ma", async (params = {}) => {
|
|
387
|
+
const request = validateProtocolRequest(params);
|
|
388
|
+
if (request.action === "queue") {
|
|
389
|
+
if (!hasProtocolAuthority(params, settings)) {
|
|
390
|
+
throw new Error("obsidian_protocol_queue_authority_required");
|
|
391
|
+
}
|
|
392
|
+
return writeQueueRequestToVault({ app, request, settings });
|
|
393
|
+
}
|
|
394
|
+
if (request.action === "capture") {
|
|
395
|
+
if (!hasProtocolAuthority(request, settings)) {
|
|
396
|
+
throw new Error("obsidian_protocol_authority_required");
|
|
397
|
+
}
|
|
398
|
+
await captureActiveNoteToVault({ app, MarkdownView, settings });
|
|
399
|
+
return protocolReceipt("capture", request);
|
|
400
|
+
}
|
|
401
|
+
if (!hasProtocolAuthority(request, settings)) {
|
|
402
|
+
throw new Error("obsidian_protocol_authority_required");
|
|
403
|
+
}
|
|
404
|
+
const target = request.path ?? "Meta-Architect/Map of Content.md";
|
|
405
|
+
assertSafeVaultPath(target);
|
|
406
|
+
const file = app?.vault?.getAbstractFileByPath?.(target);
|
|
407
|
+
if (file) await app?.workspace?.getLeaf?.(true)?.openFile?.(file);
|
|
408
|
+
return protocolReceipt("open", request);
|
|
409
|
+
});
|
|
410
|
+
return { registered: true, handler: "ma", supports: ["open", "queue", "capture"] };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function registerObsidianEventWatchers({ plugin, app, onEvent = () => {} } = {}) {
|
|
414
|
+
const registrations = [];
|
|
415
|
+
const register = (source, eventName, eventRef) => {
|
|
416
|
+
if (!eventRef) return;
|
|
417
|
+
plugin?.registerEvent?.(eventRef);
|
|
418
|
+
registrations.push({ source, event: eventName });
|
|
419
|
+
};
|
|
420
|
+
const vault = app?.vault;
|
|
421
|
+
const metadataCache = app?.metadataCache;
|
|
422
|
+
const workspace = app?.workspace;
|
|
423
|
+
|
|
424
|
+
for (const eventName of ["create", "modify", "delete", "rename"]) {
|
|
425
|
+
register(
|
|
426
|
+
"vault",
|
|
427
|
+
eventName,
|
|
428
|
+
vault?.on?.(eventName, (...args) => onEvent({ source: "vault", eventName, args })),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
for (const eventName of ["resolved", "changed", "deleted"]) {
|
|
432
|
+
register(
|
|
433
|
+
"metadataCache",
|
|
434
|
+
eventName,
|
|
435
|
+
metadataCache?.on?.(eventName, (...args) =>
|
|
436
|
+
onEvent({ source: "metadataCache", eventName, args }),
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
for (const eventName of ["active-leaf-change", "layout-change"]) {
|
|
441
|
+
register(
|
|
442
|
+
"workspace",
|
|
443
|
+
eventName,
|
|
444
|
+
workspace?.on?.(eventName, (...args) => onEvent({ source: "workspace", eventName, args })),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
record_type: "obsidian_event_watchers",
|
|
450
|
+
records_as: "vault_context",
|
|
451
|
+
build_evidence: false,
|
|
452
|
+
registered_count: registrations.length,
|
|
453
|
+
registrations,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export async function extractObsidianCanvasContext({ app, files } = {}) {
|
|
458
|
+
const vault = app?.vault;
|
|
459
|
+
const candidatePaths =
|
|
460
|
+
files?.map((file) => file.path ?? file).filter(Boolean) ??
|
|
461
|
+
app?.metadataCache?.getCachedFiles?.()?.filter((filePath) => filePath.endsWith(".canvas")) ??
|
|
462
|
+
[];
|
|
463
|
+
const canvases = [];
|
|
464
|
+
|
|
465
|
+
for (const filePath of candidatePaths) {
|
|
466
|
+
if (!filePath.endsWith(".canvas")) continue;
|
|
467
|
+
const file = vault?.getAbstractFileByPath?.(filePath) ?? { path: filePath };
|
|
468
|
+
const raw = await vault?.read?.(file);
|
|
469
|
+
if (typeof raw !== "string") continue;
|
|
470
|
+
const parsed = JSON.parse(raw);
|
|
471
|
+
canvases.push({
|
|
472
|
+
path: filePath,
|
|
473
|
+
records_as: "vault_context",
|
|
474
|
+
build_evidence: false,
|
|
475
|
+
nodes: parsed.nodes ?? [],
|
|
476
|
+
edges: parsed.edges ?? [],
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
record_type: "obsidian_canvas_context",
|
|
482
|
+
records_as: "vault_context",
|
|
483
|
+
build_evidence: false,
|
|
484
|
+
canvas_count: canvases.length,
|
|
485
|
+
canvases,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export async function writeObsidianAttachment({
|
|
490
|
+
app,
|
|
491
|
+
attachmentPath,
|
|
492
|
+
data,
|
|
493
|
+
contentType = "application/octet-stream",
|
|
494
|
+
overwrite = false,
|
|
495
|
+
maxBytes = defaultAttachmentMaxBytes,
|
|
496
|
+
allowedContentTypes = defaultAttachmentContentTypes,
|
|
497
|
+
requestId = null,
|
|
498
|
+
sourceAction = null,
|
|
499
|
+
} = {}) {
|
|
500
|
+
assertSafeVaultPath(attachmentPath);
|
|
501
|
+
assertMaOwnedPath(attachmentPath, "Meta-Architect/Attachments/");
|
|
502
|
+
if (!attachmentPath) {
|
|
503
|
+
throw new Error("Obsidian attachment write requires attachmentPath");
|
|
504
|
+
}
|
|
505
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
506
|
+
throw new Error("attachment_max_bytes_invalid");
|
|
507
|
+
}
|
|
508
|
+
const bytes = toUint8Array(data);
|
|
509
|
+
if (bytes.byteLength > maxBytes) {
|
|
510
|
+
throw new Error(`attachment_too_large:${attachmentPath}:${bytes.byteLength}`);
|
|
511
|
+
}
|
|
512
|
+
if (!isAllowedAttachmentContentType(contentType, allowedContentTypes)) {
|
|
513
|
+
throw new Error(`attachment_type_forbidden:${contentType}`);
|
|
514
|
+
}
|
|
515
|
+
const parent = attachmentPath.includes("/")
|
|
516
|
+
? attachmentPath.slice(0, attachmentPath.lastIndexOf("/"))
|
|
517
|
+
: "";
|
|
518
|
+
if (parent) await ensureVaultFolder(app, parent);
|
|
519
|
+
const existing = app?.vault?.getAbstractFileByPath?.(attachmentPath);
|
|
520
|
+
if (existing && !overwrite) {
|
|
521
|
+
throw new Error(`attachment_exists:${attachmentPath}`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (existing && app?.vault?.adapter?.writeBinary) {
|
|
525
|
+
await app.vault.adapter.writeBinary(attachmentPath, bytes);
|
|
526
|
+
} else if (app?.vault?.createBinary) {
|
|
527
|
+
await app.vault.createBinary(
|
|
528
|
+
attachmentPath,
|
|
529
|
+
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
|
|
530
|
+
);
|
|
531
|
+
} else if (app?.vault?.adapter?.writeBinary) {
|
|
532
|
+
await app.vault.adapter.writeBinary(attachmentPath, bytes);
|
|
533
|
+
} else {
|
|
534
|
+
throw new Error("Obsidian Vault.createBinary or DataAdapter.writeBinary is required");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
record_type: "obsidian_attachment_write",
|
|
539
|
+
records_as: "vault_context",
|
|
540
|
+
build_evidence: false,
|
|
541
|
+
attachment_path: attachmentPath,
|
|
542
|
+
content_type: contentType,
|
|
543
|
+
byte_length: bytes.byteLength,
|
|
544
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
545
|
+
request_id: requestId,
|
|
546
|
+
source_action: sourceAction,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export async function drainObsidianPluginRequestQueue({
|
|
551
|
+
app,
|
|
552
|
+
queue = [],
|
|
553
|
+
MarkdownView,
|
|
554
|
+
settings = {},
|
|
555
|
+
} = {}) {
|
|
556
|
+
if (!Array.isArray(queue)) {
|
|
557
|
+
throw new Error("Obsidian plugin request queue must be an array");
|
|
558
|
+
}
|
|
559
|
+
const processed = [];
|
|
560
|
+
const refused = [];
|
|
561
|
+
const refusedQueueItems = [];
|
|
562
|
+
|
|
563
|
+
for (const request of queue) {
|
|
564
|
+
try {
|
|
565
|
+
if (!request || typeof request !== "object") {
|
|
566
|
+
throw new Error("invalid_request");
|
|
567
|
+
}
|
|
568
|
+
if (
|
|
569
|
+
["capture_active_note", "create_note", "rename_note", "write_attachment"].includes(
|
|
570
|
+
request.action,
|
|
571
|
+
) &&
|
|
572
|
+
!hasProtocolAuthority(request, settings)
|
|
573
|
+
) {
|
|
574
|
+
throw new Error("obsidian_queue_authority_required");
|
|
575
|
+
}
|
|
576
|
+
const target = request.note_path ?? request.path ?? "";
|
|
577
|
+
if (target) assertSafeVaultPath(target);
|
|
578
|
+
switch (request.action) {
|
|
579
|
+
case "capture_active_note":
|
|
580
|
+
processed.push({
|
|
581
|
+
request_id: request.id ?? null,
|
|
582
|
+
action: request.action,
|
|
583
|
+
result: await captureActiveNoteToVault({ app, MarkdownView, settings }),
|
|
584
|
+
});
|
|
585
|
+
break;
|
|
586
|
+
case "create_note":
|
|
587
|
+
processed.push({
|
|
588
|
+
request_id: request.id ?? null,
|
|
589
|
+
action: request.action,
|
|
590
|
+
result: await createVaultContextNote({
|
|
591
|
+
app,
|
|
592
|
+
notePath: target,
|
|
593
|
+
content: request.content,
|
|
594
|
+
}),
|
|
595
|
+
});
|
|
596
|
+
break;
|
|
597
|
+
case "index_vault":
|
|
598
|
+
processed.push({
|
|
599
|
+
request_id: request.id ?? null,
|
|
600
|
+
action: request.action,
|
|
601
|
+
result: createObsidianMetadataGraph({ app }),
|
|
602
|
+
});
|
|
603
|
+
break;
|
|
604
|
+
case "rename_note":
|
|
605
|
+
processed.push({
|
|
606
|
+
request_id: request.id ?? null,
|
|
607
|
+
action: request.action,
|
|
608
|
+
result: await renameObsidianFileSafely({
|
|
609
|
+
app,
|
|
610
|
+
file: app?.vault?.getAbstractFileByPath?.(request.from),
|
|
611
|
+
newPath: request.to,
|
|
612
|
+
}),
|
|
613
|
+
});
|
|
614
|
+
break;
|
|
615
|
+
case "write_attachment":
|
|
616
|
+
processed.push({
|
|
617
|
+
request_id: request.id ?? null,
|
|
618
|
+
action: request.action,
|
|
619
|
+
result: await writeObsidianAttachment({
|
|
620
|
+
app,
|
|
621
|
+
attachmentPath: target,
|
|
622
|
+
data: request.data ?? "",
|
|
623
|
+
contentType: request.content_type,
|
|
624
|
+
overwrite: request.overwrite ?? false,
|
|
625
|
+
maxBytes: defaultAttachmentMaxBytes,
|
|
626
|
+
allowedContentTypes: defaultAttachmentContentTypes,
|
|
627
|
+
requestId: request.id ?? null,
|
|
628
|
+
sourceAction: request.action,
|
|
629
|
+
}),
|
|
630
|
+
});
|
|
631
|
+
break;
|
|
632
|
+
default:
|
|
633
|
+
throw new Error(`unsupported_action:${request.action}`);
|
|
634
|
+
}
|
|
635
|
+
} catch (error) {
|
|
636
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
637
|
+
const previousAttempts = Number.isInteger(request?.attempts) ? request.attempts : 0;
|
|
638
|
+
refusedQueueItems.push({
|
|
639
|
+
...safeClone(request),
|
|
640
|
+
queue_status: "refused",
|
|
641
|
+
refusal_reason: reason,
|
|
642
|
+
attempts: previousAttempts + 1,
|
|
643
|
+
last_attempt_at: new Date().toISOString(),
|
|
644
|
+
});
|
|
645
|
+
refused.push({
|
|
646
|
+
request_id: request?.id ?? null,
|
|
647
|
+
action: request?.action ?? null,
|
|
648
|
+
reason,
|
|
649
|
+
records_as: "vault_context",
|
|
650
|
+
build_evidence: false,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
record_type: "obsidian_plugin_queue_drain",
|
|
657
|
+
records_as: "vault_context",
|
|
658
|
+
build_evidence: false,
|
|
659
|
+
drained_at: new Date().toISOString(),
|
|
660
|
+
processed,
|
|
661
|
+
refused,
|
|
662
|
+
refusedQueueItems,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function captureActiveNoteToVault({ app, Notice, MarkdownView, settings = {} } = {}) {
|
|
667
|
+
const context = createObsidianSelectionContext({ app, MarkdownView });
|
|
668
|
+
const activeFile = app?.workspace?.getActiveFile?.();
|
|
669
|
+
if (activeFile) {
|
|
670
|
+
await applyObsidianFrontmatterAuthority({
|
|
671
|
+
app,
|
|
672
|
+
file: activeFile,
|
|
673
|
+
project: settings.projectName ?? "Meta-Architect",
|
|
674
|
+
sourceWorkspace: settings.sourceWorkspace ?? null,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const content = renderActiveContextNote(context);
|
|
678
|
+
const result = await createVaultContextNote({
|
|
679
|
+
app,
|
|
680
|
+
notePath: settings.activeContextPath ?? obsidianPluginActiveContextPath,
|
|
681
|
+
content,
|
|
682
|
+
overwrite: true,
|
|
683
|
+
});
|
|
684
|
+
if (Notice) {
|
|
685
|
+
new Notice("MA captured current Obsidian context");
|
|
686
|
+
}
|
|
687
|
+
return { ...result, active_context: context };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
let obsidianQueueBusy = Promise.resolve();
|
|
691
|
+
|
|
692
|
+
function withObsidianQueueLock(task) {
|
|
693
|
+
const previous = obsidianQueueBusy;
|
|
694
|
+
let release;
|
|
695
|
+
obsidianQueueBusy = new Promise((resolve) => {
|
|
696
|
+
release = resolve;
|
|
697
|
+
});
|
|
698
|
+
return previous.then(task).finally(release);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function drainQueueFromVault(args) {
|
|
702
|
+
return withObsidianQueueLock(() => drainQueueFromVaultUnlocked(args));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function drainQueueFromVaultUnlocked({ app, MarkdownView, settings = {} }) {
|
|
706
|
+
const queuePath = settings.queuePath ?? obsidianPluginQueuePath;
|
|
707
|
+
const file = app?.vault?.getAbstractFileByPath?.(queuePath);
|
|
708
|
+
if (!file) {
|
|
709
|
+
return drainObsidianPluginRequestQueue({ app, queue: [], MarkdownView, settings });
|
|
710
|
+
}
|
|
711
|
+
const raw = await app.vault.read(file);
|
|
712
|
+
const queue = JSON.parse(raw);
|
|
713
|
+
const result = await drainObsidianPluginRequestQueue({ app, queue, MarkdownView, settings });
|
|
714
|
+
const nextQueue = `${JSON.stringify(result.refusedQueueItems, null, 2)}\n`;
|
|
715
|
+
if (raw !== nextQueue) await app.vault.modify(file, nextQueue);
|
|
716
|
+
return result;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function writeQueueRequestToVault(args) {
|
|
720
|
+
return withObsidianQueueLock(() => writeQueueRequestToVaultUnlocked(args));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function writeQueueRequestToVaultUnlocked({ app, request, settings = {} }) {
|
|
724
|
+
if (!hasProtocolAuthority(request, settings)) {
|
|
725
|
+
throw new Error("obsidian_protocol_queue_authority_required");
|
|
726
|
+
}
|
|
727
|
+
const queuePath = obsidianPluginQueuePath;
|
|
728
|
+
await ensureVaultFolder(app, path.dirname(queuePath));
|
|
729
|
+
const file = app?.vault?.getAbstractFileByPath?.(queuePath);
|
|
730
|
+
const existing = file ? JSON.parse(await app.vault.read(file)) : [];
|
|
731
|
+
const queuedRequest = Object.fromEntries(
|
|
732
|
+
Object.entries(request).filter(([key]) => key !== "protocol_token"),
|
|
733
|
+
);
|
|
734
|
+
existing.push({
|
|
735
|
+
...queuedRequest,
|
|
736
|
+
id: request.id ?? `ma-${Date.now()}`,
|
|
737
|
+
queued_at: new Date().toISOString(),
|
|
738
|
+
records_as: "vault_context",
|
|
739
|
+
protocol_token_hash: request.protocol_token_hash ?? tokenHash(request.protocol_token),
|
|
740
|
+
});
|
|
741
|
+
const content = `${JSON.stringify(existing, null, 2)}\n`;
|
|
742
|
+
if (file) await app.vault.modify(file, content);
|
|
743
|
+
else await app.vault.create(queuePath, content);
|
|
744
|
+
return protocolReceipt("queue", request);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function createVaultContextNote({ app, notePath, content, overwrite = true }) {
|
|
748
|
+
assertSafeVaultPath(notePath);
|
|
749
|
+
assertMaOwnedPath(notePath, "Meta-Architect/");
|
|
750
|
+
if (typeof content !== "string" || content.trim().length === 0) {
|
|
751
|
+
throw new Error("vault_context note content required");
|
|
752
|
+
}
|
|
753
|
+
await ensureVaultFolder(app, path.dirname(notePath));
|
|
754
|
+
const normalizedContent = ensureVaultContextFrontmatter(content);
|
|
755
|
+
const existing = app?.vault?.getAbstractFileByPath?.(notePath);
|
|
756
|
+
if (existing && overwrite) {
|
|
757
|
+
await app.vault.modify(existing, normalizedContent);
|
|
758
|
+
} else if (existing) {
|
|
759
|
+
throw new Error(`note_exists:${notePath}`);
|
|
760
|
+
} else {
|
|
761
|
+
await app.vault.create(notePath, normalizedContent);
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
record_type: "obsidian_vault_context_note_write",
|
|
765
|
+
records_as: "vault_context",
|
|
766
|
+
build_evidence: false,
|
|
767
|
+
note_path: notePath,
|
|
768
|
+
char_count: normalizedContent.length,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function ensureVaultFolder(app, folderPath) {
|
|
773
|
+
if (!folderPath || folderPath === ".") return;
|
|
774
|
+
const segments = folderPath.split("/").filter(Boolean);
|
|
775
|
+
let current = "";
|
|
776
|
+
for (const segment of segments) {
|
|
777
|
+
current = current ? `${current}/${segment}` : segment;
|
|
778
|
+
if (!app?.vault?.getAbstractFileByPath?.(current)) {
|
|
779
|
+
await app?.vault?.createFolder?.(current).catch(() => {});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function renderActiveContextNote(context) {
|
|
785
|
+
return ensureVaultContextFrontmatter(`# Active Note Context
|
|
786
|
+
|
|
787
|
+
- Active note: ${context.active_note_path ? `[[${context.active_note_path.replace(/\.md$/i, "")}]]` : "none"}
|
|
788
|
+
- View type: ${context.view_type ?? "unknown"}
|
|
789
|
+
- Cursor: ${JSON.stringify(context.cursor)}
|
|
790
|
+
- Selection chars: ${context.selected_text_char_count}
|
|
791
|
+
|
|
792
|
+
## Selected Text
|
|
793
|
+
|
|
794
|
+
${context.selected_text || "_No active selection captured._"}
|
|
795
|
+
|
|
796
|
+
## Pane State
|
|
797
|
+
|
|
798
|
+
\`\`\`json
|
|
799
|
+
${JSON.stringify(context.pane_state ?? {}, null, 2)}
|
|
800
|
+
\`\`\`
|
|
801
|
+
`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function ensureVaultContextFrontmatter(content) {
|
|
805
|
+
if (content.trimStart().startsWith("---")) {
|
|
806
|
+
return content.replace(
|
|
807
|
+
/^---\n/,
|
|
808
|
+
"---\nma_records_as: vault_context\nma_project: Meta-Architect\n",
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
return `---
|
|
812
|
+
ma_records_as: vault_context
|
|
813
|
+
ma_project: Meta-Architect
|
|
814
|
+
ma_capabilities:
|
|
815
|
+
- obsidian_integration_core
|
|
816
|
+
---
|
|
817
|
+
|
|
818
|
+
${content}`;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function assertSafeVaultPath(targetPath) {
|
|
822
|
+
if (!targetPath || typeof targetPath !== "string") {
|
|
823
|
+
throw new Error("vault_path_required");
|
|
824
|
+
}
|
|
825
|
+
const normalized = targetPath.replaceAll("\\", "/").replace(/^\/+/, "");
|
|
826
|
+
if (normalized.includes("../") || normalized === ".." || normalized.startsWith("..")) {
|
|
827
|
+
throw new Error("path_traversal_forbidden");
|
|
828
|
+
}
|
|
829
|
+
if ([".obsidian/", ".trash/", ".git/"].some((prefix) => normalized.startsWith(prefix))) {
|
|
830
|
+
throw new Error("vault_control_path_forbidden");
|
|
831
|
+
}
|
|
832
|
+
for (const forbidden of forbiddenMaTargets) {
|
|
833
|
+
if (normalized === forbidden || normalized.startsWith(forbidden)) {
|
|
834
|
+
throw new Error(`forbidden_authoritative_mutation:${forbidden}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function assertMaOwnedPath(targetPath, prefix) {
|
|
840
|
+
const normalized = targetPath.replaceAll("\\", "/").replace(/^\/+/, "");
|
|
841
|
+
if (!normalized.startsWith(prefix)) throw new Error(`ma_owned_path_required:${prefix}`);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function hasVaultContextAuthority(params, settings) {
|
|
845
|
+
const requiredAuthority = settings.protocolAuthority ?? "vault_context";
|
|
846
|
+
return params.authority === requiredAuthority;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function hasProtocolAuthority(params, settings) {
|
|
850
|
+
return (
|
|
851
|
+
Boolean(settings.protocolToken) &&
|
|
852
|
+
(params?.protocol_token === settings.protocolToken ||
|
|
853
|
+
params?.protocol_token_hash === tokenHash(settings.protocolToken)) &&
|
|
854
|
+
hasVaultContextAuthority(params, settings)
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function validateProtocolRequest(params) {
|
|
859
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
860
|
+
throw new Error("obsidian_protocol_request_invalid");
|
|
861
|
+
}
|
|
862
|
+
const serialized = JSON.stringify(params);
|
|
863
|
+
if (Buffer.byteLength(serialized, "utf8") > protocolMaxPayloadBytes) {
|
|
864
|
+
throw new Error("obsidian_protocol_payload_too_large");
|
|
865
|
+
}
|
|
866
|
+
for (const key of Object.keys(params)) {
|
|
867
|
+
if (!protocolFields.has(key)) throw new Error(`obsidian_protocol_field_forbidden:${key}`);
|
|
868
|
+
}
|
|
869
|
+
const action = params.action ?? (params.open === "active" ? "capture" : null);
|
|
870
|
+
if (!protocolActions.has(action)) throw new Error("obsidian_protocol_action_forbidden");
|
|
871
|
+
if (action === "open" && params.open === "active") {
|
|
872
|
+
throw new Error("obsidian_protocol_action_invalid");
|
|
873
|
+
}
|
|
874
|
+
const request = { ...params, action };
|
|
875
|
+
for (const target of [request.path, request.note_path, request.from, request.to]) {
|
|
876
|
+
if (target !== undefined) assertSafeVaultPath(target);
|
|
877
|
+
}
|
|
878
|
+
if (request.content !== undefined && typeof request.content !== "string") {
|
|
879
|
+
throw new Error("obsidian_protocol_content_invalid");
|
|
880
|
+
}
|
|
881
|
+
if (request.data !== undefined && typeof request.data !== "string") {
|
|
882
|
+
throw new Error("obsidian_protocol_data_invalid");
|
|
883
|
+
}
|
|
884
|
+
return request;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function protocolReceipt(action, request) {
|
|
888
|
+
return {
|
|
889
|
+
record_type: "obsidian_protocol_action",
|
|
890
|
+
records_as: "vault_context",
|
|
891
|
+
build_evidence: false,
|
|
892
|
+
action,
|
|
893
|
+
request_id: request.id ?? null,
|
|
894
|
+
status: "accepted",
|
|
895
|
+
authority: request.authority,
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function tokenHash(token) {
|
|
900
|
+
return createHash("sha256")
|
|
901
|
+
.update(String(token ?? ""))
|
|
902
|
+
.digest("hex");
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async function hashFile(filePath) {
|
|
906
|
+
return createHash("sha256")
|
|
907
|
+
.update(await fs.readFile(filePath))
|
|
908
|
+
.digest("hex");
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async function assertNoSymlinkComponents(root, target) {
|
|
912
|
+
const rootPath = path.resolve(root);
|
|
913
|
+
const rootStat = await fs.lstat(rootPath);
|
|
914
|
+
if (rootStat.isSymbolicLink()) throw new Error("Obsidian vault root cannot be a symlink");
|
|
915
|
+
let current = rootPath;
|
|
916
|
+
for (const part of path
|
|
917
|
+
.relative(rootPath, path.resolve(target))
|
|
918
|
+
.split(path.sep)
|
|
919
|
+
.filter(Boolean)) {
|
|
920
|
+
current = path.join(current, part);
|
|
921
|
+
const stat = await fs.lstat(current).catch((error) => {
|
|
922
|
+
if (error?.code === "ENOENT") return null;
|
|
923
|
+
throw error;
|
|
924
|
+
});
|
|
925
|
+
if (stat?.isSymbolicLink())
|
|
926
|
+
throw new Error(`Obsidian plugin path cannot traverse a symlink: ${current}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function isAllowedAttachmentContentType(contentType, allowedContentTypes) {
|
|
931
|
+
if (!contentType || typeof contentType !== "string") {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
if (allowedContentTypes instanceof Set) {
|
|
935
|
+
return allowedContentTypes.has(contentType);
|
|
936
|
+
}
|
|
937
|
+
if (Array.isArray(allowedContentTypes)) {
|
|
938
|
+
return allowedContentTypes.includes(contentType);
|
|
939
|
+
}
|
|
940
|
+
return defaultAttachmentContentTypes.has(contentType);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function safeClone(value) {
|
|
944
|
+
try {
|
|
945
|
+
return structuredClone(value);
|
|
946
|
+
} catch {
|
|
947
|
+
return JSON.parse(JSON.stringify(value ?? null));
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function toUint8Array(data) {
|
|
952
|
+
if (data instanceof Uint8Array) return data;
|
|
953
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
954
|
+
if (ArrayBuffer.isView(data)) {
|
|
955
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
956
|
+
}
|
|
957
|
+
return new TextEncoder().encode(String(data ?? ""));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function safeCall(fn) {
|
|
961
|
+
try {
|
|
962
|
+
return fn();
|
|
963
|
+
} catch {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function basenameWithoutExtension(filePath) {
|
|
969
|
+
if (!filePath) return null;
|
|
970
|
+
return path.basename(filePath, path.extname(filePath));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function getPackageRoot() {
|
|
974
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async function listPluginFiles(root) {
|
|
978
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
979
|
+
const files = [];
|
|
980
|
+
for (const entry of entries) {
|
|
981
|
+
const absolutePath = path.join(root, entry.name);
|
|
982
|
+
if (entry.isDirectory()) {
|
|
983
|
+
const nested = await listPluginFiles(absolutePath);
|
|
984
|
+
files.push(...nested.map((file) => `${entry.name}/${file}`));
|
|
985
|
+
} else {
|
|
986
|
+
files.push(entry.name);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return files.sort();
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
async function enableObsidianCommunityPlugin({ vaultPath, pluginId }) {
|
|
993
|
+
const configPath = path.join(vaultPath, ".obsidian", "community-plugins.json");
|
|
994
|
+
await assertNoSymlinkComponents(vaultPath, configPath);
|
|
995
|
+
const enabled = parseCommunityPlugins(await readOptionalFile(configPath));
|
|
996
|
+
if (!enabled.includes(pluginId)) {
|
|
997
|
+
enabled.push(pluginId);
|
|
998
|
+
}
|
|
999
|
+
await fs.writeFile(configPath, `${JSON.stringify(enabled, null, 2)}\n`, "utf8");
|
|
1000
|
+
return enabled;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async function readOptionalFile(filePath) {
|
|
1004
|
+
try {
|
|
1005
|
+
return await fs.readFile(filePath, "utf8");
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
if (error?.code === "ENOENT") return null;
|
|
1008
|
+
throw error;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function pathExists(filePath) {
|
|
1013
|
+
try {
|
|
1014
|
+
await fs.lstat(filePath);
|
|
1015
|
+
return true;
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
if (error?.code === "ENOENT") return false;
|
|
1018
|
+
throw error;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function parseCommunityPlugins(content) {
|
|
1023
|
+
if (content === null) return [];
|
|
1024
|
+
const parsed = JSON.parse(content);
|
|
1025
|
+
return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function createMetaArchitectSettingTab(app, plugin, PluginSettingTab, Setting) {
|
|
1029
|
+
const instance = new PluginSettingTab(app, plugin);
|
|
1030
|
+
instance.display = () => {
|
|
1031
|
+
const { containerEl } = instance;
|
|
1032
|
+
containerEl.empty?.();
|
|
1033
|
+
containerEl.createEl?.("h2", { text: "Meta-Architect" });
|
|
1034
|
+
new Setting(containerEl)
|
|
1035
|
+
.setName("Vault context mode")
|
|
1036
|
+
.setDesc("Obsidian notes are recorded as vault_context, never build_evidence.")
|
|
1037
|
+
.addText?.((text) =>
|
|
1038
|
+
text
|
|
1039
|
+
.setPlaceholder("Meta-Architect")
|
|
1040
|
+
.setValue?.("Meta-Architect")
|
|
1041
|
+
.onChange?.(async () => {}),
|
|
1042
|
+
);
|
|
1043
|
+
};
|
|
1044
|
+
return instance;
|
|
1045
|
+
}
|