@pingvinen/donna-assistant 0.9.0 → 0.10.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/README.md +31 -3
- package/package.json +1 -1
- package/src/donna-tools.cjs +429 -0
- package/src/installer.cjs +26 -4
- package/stubs/claude-code/donna/remove-tool.md +17 -0
- package/workflows/add-task.md +15 -75
- package/workflows/add-tool.md +58 -91
- package/workflows/adjust-tool.md +22 -132
- package/workflows/begin-the-day.md +22 -92
- package/workflows/done.md +16 -74
- package/workflows/focus.md +21 -95
- package/workflows/relearn-tools.md +60 -91
- package/workflows/remove-tool.md +93 -0
- package/workflows/run-tools.md +26 -99
- package/workflows/set-role.md +12 -61
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ For CLI tools, Donna verifies installation and authentication, learns capabiliti
|
|
|
92
92
|
|
|
93
93
|
When multiple tools are registered, Donna runs them in parallel — one agent per tool — so your morning brief stays fast regardless of how many tools you have.
|
|
94
94
|
|
|
95
|
-
Edit a tool's configuration (scope, capabilities, auth, command
|
|
95
|
+
Edit a tool's configuration (scope, capabilities, auth, command) at any time:
|
|
96
96
|
|
|
97
97
|
```
|
|
98
98
|
/donna:adjust-tool
|
|
@@ -126,22 +126,50 @@ The storage repo is yours. Donna only writes to the `donna/` and `daily/` direct
|
|
|
126
126
|
|
|
127
127
|
Most commands are safe to run again. Want to update your role? Run `/donna:set-role` again. Added a new tool mid-day? Run `/donna:begin-the-day` again to pull its data in. Re-running a command updates state rather than duplicating it.
|
|
128
128
|
|
|
129
|
+
## Why not automate tool pulls?
|
|
130
|
+
|
|
131
|
+
Donna intentionally does not support automated periodic invocations of `/donna:run-tools` (e.g., via cron or background timers). Each tool pull runs inside an AI coding assistant session, which means:
|
|
132
|
+
|
|
133
|
+
- **Cost** -- every invocation consumes API tokens. A cron job running hourly would burn tokens even when you are not working.
|
|
134
|
+
- **Context** -- tool data is most useful when you are actively looking at it. Pulling data while you are away produces stale results by the time you return.
|
|
135
|
+
- **Conflicts** -- background runs could write to your daily file while you are editing it, causing git merge conflicts in your storage repo.
|
|
136
|
+
|
|
137
|
+
Instead, run `/donna:run-tools` on demand when you want fresh data, or let `/donna:begin-the-day` handle it as part of your morning routine.
|
|
138
|
+
|
|
129
139
|
## All commands
|
|
130
140
|
|
|
141
|
+
### Setup and configuration
|
|
142
|
+
|
|
131
143
|
| Command | What it does |
|
|
132
144
|
|---------|-------------|
|
|
133
145
|
| `/donna:setup` | First-time configuration (storage repo, directories) |
|
|
134
146
|
| `/donna:set-role` | Define your job role, get recurring task suggestions |
|
|
147
|
+
|
|
148
|
+
### Daily workflow
|
|
149
|
+
|
|
150
|
+
| Command | What it does |
|
|
151
|
+
|---------|-------------|
|
|
135
152
|
| `/donna:begin-the-day` | Morning brief with carry-forward, recurring tasks, tool data |
|
|
136
153
|
| `/donna:add-task` | Capture a task instantly |
|
|
137
154
|
| `/donna:done` | Mark a task complete |
|
|
155
|
+
| `/donna:focus` | Distill today's tasks into a short prioritized focus list |
|
|
156
|
+
|
|
157
|
+
### Tool management
|
|
158
|
+
|
|
159
|
+
| Command | What it does |
|
|
160
|
+
|---------|-------------|
|
|
138
161
|
| `/donna:add-tool` | Register an external tool (CLI, REST API, GraphQL API, MCP server) |
|
|
139
|
-
| `/donna:adjust-tool` | Edit a tool's configuration (scope, capabilities, auth, command
|
|
162
|
+
| `/donna:adjust-tool` | Edit a tool's configuration (scope, capabilities, auth, command) |
|
|
163
|
+
| `/donna:remove-tool` | Remove a registered tool from tools.md |
|
|
140
164
|
| `/donna:run-tools` | Refresh tool data mid-day |
|
|
141
165
|
| `/donna:relearn-tools` | Update CLI tool knowledge after upgrades |
|
|
166
|
+
|
|
167
|
+
### Help and feedback
|
|
168
|
+
|
|
169
|
+
| Command | What it does |
|
|
170
|
+
|---------|-------------|
|
|
142
171
|
| `/donna:help` | Conversational troubleshooting for config, storage, or skill issues |
|
|
143
172
|
| `/donna:contribute-idea` | Submit a feature idea or bug report via GitHub Issues |
|
|
144
|
-
| `/donna:focus` | Distill today's tasks into a short prioritized focus list |
|
|
145
173
|
|
|
146
174
|
## License
|
|
147
175
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const https = require("node:https");
|
|
7
|
+
const { execSync } = require("node:child_process");
|
|
8
|
+
|
|
9
|
+
const { readVersion } = require("./version.cjs");
|
|
10
|
+
const { runMigrations } = require("./migrator.cjs");
|
|
11
|
+
const { semverGt } = require("./changelog.cjs");
|
|
12
|
+
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
// Config reader
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Read and parse ~/.config/donna/config.md YAML frontmatter.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} homeDir - Home directory override (for testing)
|
|
21
|
+
* @returns {{ storage_repo: string|null, daily_folder: string, auto_push: boolean }|null}
|
|
22
|
+
*/
|
|
23
|
+
function readConfig(homeDir) {
|
|
24
|
+
const configPath = path.join(homeDir, ".config", "donna", "config.md");
|
|
25
|
+
if (!fs.existsSync(configPath)) return null;
|
|
26
|
+
|
|
27
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
28
|
+
|
|
29
|
+
const storage_repo = content.match(/^storage_repo:\s*(.+)$/m)?.[1]?.trim() || null;
|
|
30
|
+
const daily_folder = content.match(/^daily_folder:\s*(.+)$/m)?.[1]?.trim() || "daily";
|
|
31
|
+
const auto_push_str = content.match(/^auto_push:\s*(.+)$/m)?.[1]?.trim() || "false";
|
|
32
|
+
const auto_push = auto_push_str === "true";
|
|
33
|
+
|
|
34
|
+
return { storage_repo, daily_folder, auto_push };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
// Version check
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Fetch the latest version from npm registry.
|
|
43
|
+
* Returns the latest version string or null on any failure.
|
|
44
|
+
*
|
|
45
|
+
* @param {number} [timeout=3000] - Socket timeout in ms
|
|
46
|
+
* @returns {Promise<string|null>}
|
|
47
|
+
*/
|
|
48
|
+
function fetchLatestVersionFromRegistry(timeout = 3000) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const req = https.get(
|
|
51
|
+
"https://registry.npmjs.org/@pingvinen%2Fdonna-assistant",
|
|
52
|
+
{ headers: { Accept: "application/json" } },
|
|
53
|
+
(res) => {
|
|
54
|
+
if (res.statusCode !== 200) {
|
|
55
|
+
resolve(null);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let data = "";
|
|
59
|
+
res.on("data", (chunk) => {
|
|
60
|
+
data += chunk;
|
|
61
|
+
});
|
|
62
|
+
res.on("end", () => {
|
|
63
|
+
try {
|
|
64
|
+
const latest = JSON.parse(data)["dist-tags"]?.latest || null;
|
|
65
|
+
resolve(latest);
|
|
66
|
+
} catch {
|
|
67
|
+
resolve(null);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
req.setTimeout(timeout, () => {
|
|
73
|
+
req.destroy();
|
|
74
|
+
resolve(null);
|
|
75
|
+
});
|
|
76
|
+
req.on("error", () => resolve(null));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read version-check.md cache from donnaDir.
|
|
82
|
+
* Returns { date, latest } or null if file doesn't exist or can't be parsed.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} donnaDir
|
|
85
|
+
* @returns {{ date: string, latest: string }|null}
|
|
86
|
+
*/
|
|
87
|
+
function readVersionCache(donnaDir) {
|
|
88
|
+
const cachePath = path.join(donnaDir, "version-check.md");
|
|
89
|
+
if (!fs.existsSync(cachePath)) return null;
|
|
90
|
+
|
|
91
|
+
const content = fs.readFileSync(cachePath, "utf8");
|
|
92
|
+
const date = content.match(/^checked_on:\s*(.+)$/m)?.[1]?.trim();
|
|
93
|
+
const latest = content.match(/^latest_version:\s*(.+)$/m)?.[1]?.trim();
|
|
94
|
+
if (!date || !latest) return null;
|
|
95
|
+
return { date, latest };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Write version-check.md cache to donnaDir.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} donnaDir
|
|
102
|
+
* @param {string} date - YYYY-MM-DD
|
|
103
|
+
* @param {string} latestVersion
|
|
104
|
+
*/
|
|
105
|
+
function writeVersionCache(donnaDir, date, latestVersion) {
|
|
106
|
+
const cachePath = path.join(donnaDir, "version-check.md");
|
|
107
|
+
fs.writeFileSync(
|
|
108
|
+
cachePath,
|
|
109
|
+
`---\nchecked_on: ${date}\nlatest_version: ${latestVersion}\n---\n`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
// Obsidian daily-notes sync
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Sync Obsidian daily-notes.json with the configured daily_folder.
|
|
119
|
+
* Mutates the configMd file if the folder differs.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} storageRepo
|
|
122
|
+
* @param {string} configPath
|
|
123
|
+
* @param {string} dailyFolder
|
|
124
|
+
* @returns {string} - Possibly updated daily_folder
|
|
125
|
+
*/
|
|
126
|
+
function syncObsidianDailyNotes(storageRepo, configPath, dailyFolder) {
|
|
127
|
+
const obsidianDir = path.join(storageRepo, ".obsidian");
|
|
128
|
+
const dailyNotesJson = path.join(obsidianDir, "daily-notes.json");
|
|
129
|
+
|
|
130
|
+
if (fs.existsSync(dailyNotesJson)) {
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(fs.readFileSync(dailyNotesJson, "utf8"));
|
|
133
|
+
if (parsed.folder && parsed.folder !== dailyFolder) {
|
|
134
|
+
// Sync config to Obsidian's value
|
|
135
|
+
const configContent = fs.readFileSync(configPath, "utf8");
|
|
136
|
+
const updated = configContent.replace(
|
|
137
|
+
/^daily_folder:\s*.+$/m,
|
|
138
|
+
`daily_folder: ${parsed.folder}`,
|
|
139
|
+
);
|
|
140
|
+
fs.writeFileSync(configPath, updated);
|
|
141
|
+
return parsed.folder;
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Parse error — ignore
|
|
145
|
+
}
|
|
146
|
+
} else if (fs.existsSync(obsidianDir)) {
|
|
147
|
+
// .obsidian/ exists but no daily-notes.json — write it
|
|
148
|
+
try {
|
|
149
|
+
fs.writeFileSync(dailyNotesJson, JSON.stringify({ folder: dailyFolder }, null, 2));
|
|
150
|
+
} catch {
|
|
151
|
+
// Ignore errors writing Obsidian config
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return dailyFolder;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
// Subcommand handlers
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Run the `init` subcommand.
|
|
164
|
+
*
|
|
165
|
+
* Reads config, runs migrations, syncs Obsidian, checks for updates.
|
|
166
|
+
*
|
|
167
|
+
* @param {string[]} _args
|
|
168
|
+
* @param {object} [options]
|
|
169
|
+
* @param {string} [options.homeDir] - Override home directory (for testing)
|
|
170
|
+
* @param {Function} [options.fetchLatestVersion] - Override for network call (for testing)
|
|
171
|
+
* @returns {Promise<object>}
|
|
172
|
+
*/
|
|
173
|
+
async function runInit(_args, options = {}) {
|
|
174
|
+
const homeDir = options.homeDir || os.homedir();
|
|
175
|
+
const donnaDir = path.join(homeDir, ".donna");
|
|
176
|
+
const migrationsDir = path.join(__dirname, "..", "migrations");
|
|
177
|
+
const fetchLatestVersion = options.fetchLatestVersion || fetchLatestVersionFromRegistry;
|
|
178
|
+
|
|
179
|
+
// Read config
|
|
180
|
+
const config = readConfig(homeDir);
|
|
181
|
+
if (!config || !config.storage_repo) {
|
|
182
|
+
return { error: "not_configured", storage_repo: null };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let { storage_repo, daily_folder, auto_push } = config;
|
|
186
|
+
|
|
187
|
+
// Sync Obsidian daily-notes
|
|
188
|
+
const configPath = path.join(homeDir, ".config", "donna", "config.md");
|
|
189
|
+
daily_folder = syncObsidianDailyNotes(storage_repo, configPath, daily_folder);
|
|
190
|
+
|
|
191
|
+
// Run migrations
|
|
192
|
+
const versionInfo = readVersion(donnaDir);
|
|
193
|
+
const lastMigration = versionInfo?.lastMigration || 0;
|
|
194
|
+
const migrationResults = runMigrations(migrationsDir, donnaDir, lastMigration);
|
|
195
|
+
const migrations_applied = migrationResults.filter((r) => r.ok).map((r) => r.description);
|
|
196
|
+
|
|
197
|
+
// Version check (once per day, non-blocking)
|
|
198
|
+
let update_available = null;
|
|
199
|
+
try {
|
|
200
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
201
|
+
const cache = readVersionCache(donnaDir);
|
|
202
|
+
|
|
203
|
+
let latestVersion;
|
|
204
|
+
if (cache && cache.date === today) {
|
|
205
|
+
// Cache hit — use cached value
|
|
206
|
+
latestVersion = cache.latest;
|
|
207
|
+
} else {
|
|
208
|
+
// Cache miss — fetch from registry
|
|
209
|
+
latestVersion = await fetchLatestVersion();
|
|
210
|
+
if (latestVersion) {
|
|
211
|
+
writeVersionCache(donnaDir, today, latestVersion);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (latestVersion) {
|
|
216
|
+
const pkg = require("../package.json");
|
|
217
|
+
const currentVersion = versionInfo?.version || pkg.version;
|
|
218
|
+
if (semverGt(latestVersion, currentVersion)) {
|
|
219
|
+
update_available = latestVersion;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Never throw from version check — treat as no update available
|
|
224
|
+
update_available = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
storage_repo,
|
|
229
|
+
daily_folder,
|
|
230
|
+
auto_push,
|
|
231
|
+
update_available,
|
|
232
|
+
migrations_applied,
|
|
233
|
+
error: null,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Run the `commit` subcommand.
|
|
239
|
+
*
|
|
240
|
+
* Args: commit <msg> --files f1 f2 ...
|
|
241
|
+
*
|
|
242
|
+
* @param {string[]} args
|
|
243
|
+
* @param {object} [options]
|
|
244
|
+
* @param {string} [options.homeDir] - Override home directory (for testing)
|
|
245
|
+
* @returns {Promise<object>}
|
|
246
|
+
*/
|
|
247
|
+
async function runCommit(args, options = {}) {
|
|
248
|
+
const homeDir = options.homeDir || os.homedir();
|
|
249
|
+
const config = readConfig(homeDir);
|
|
250
|
+
if (!config || !config.storage_repo) {
|
|
251
|
+
return { error: "not_configured" };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const { storage_repo, auto_push } = config;
|
|
255
|
+
|
|
256
|
+
// Parse message and files
|
|
257
|
+
const filesIdx = args.indexOf("--files");
|
|
258
|
+
const message = args[0] || "";
|
|
259
|
+
const files = filesIdx !== -1 ? args.slice(filesIdx + 1) : [];
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
// Stage files
|
|
263
|
+
if (files.length > 0) {
|
|
264
|
+
for (const file of files) {
|
|
265
|
+
execSync(`git -C ${JSON.stringify(storage_repo)} add ${JSON.stringify(file)}`, {
|
|
266
|
+
stdio: "pipe",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
execSync(`git -C ${JSON.stringify(storage_repo)} add -A`, { stdio: "pipe" });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check if there's anything to commit
|
|
274
|
+
const status = execSync(`git -C ${JSON.stringify(storage_repo)} status --porcelain`, {
|
|
275
|
+
stdio: "pipe",
|
|
276
|
+
encoding: "utf8",
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (!status || status.trim() === "") {
|
|
280
|
+
return { committed: false, reason: "nothing_to_commit" };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Commit
|
|
284
|
+
execSync(`git -C ${JSON.stringify(storage_repo)} commit -m ${JSON.stringify(message)}`, {
|
|
285
|
+
stdio: "pipe",
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Push if auto_push
|
|
289
|
+
if (auto_push) {
|
|
290
|
+
execSync(`git -C ${JSON.stringify(storage_repo)} push`, { stdio: "pipe" });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { committed: true, message };
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return { error: err.message, committed: false };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Run the `daily-path` subcommand.
|
|
301
|
+
*
|
|
302
|
+
* @param {string[]} _args
|
|
303
|
+
* @param {object} [options]
|
|
304
|
+
* @param {string} [options.homeDir] - Override home directory (for testing)
|
|
305
|
+
* @returns {Promise<object>}
|
|
306
|
+
*/
|
|
307
|
+
async function runDailyPath(_args, options = {}) {
|
|
308
|
+
const homeDir = options.homeDir || os.homedir();
|
|
309
|
+
const config = readConfig(homeDir);
|
|
310
|
+
if (!config || !config.storage_repo) {
|
|
311
|
+
return { error: "not_configured" };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const { storage_repo, daily_folder } = config;
|
|
315
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
316
|
+
const dailyDir = path.join(storage_repo, daily_folder);
|
|
317
|
+
const filePath = path.join(dailyDir, `${today}.md`);
|
|
318
|
+
|
|
319
|
+
// Create directory if it doesn't exist
|
|
320
|
+
fs.mkdirSync(dailyDir, { recursive: true });
|
|
321
|
+
|
|
322
|
+
return { path: filePath };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Run the `resolve-secret` subcommand.
|
|
327
|
+
*
|
|
328
|
+
* Args: resolve-secret <key>
|
|
329
|
+
*
|
|
330
|
+
* @param {string[]} args
|
|
331
|
+
* @param {object} [options]
|
|
332
|
+
* @param {string} [options.homeDir] - Override home directory (for testing)
|
|
333
|
+
* @returns {Promise<object>}
|
|
334
|
+
*/
|
|
335
|
+
async function runResolveSecret(args, options = {}) {
|
|
336
|
+
const homeDir = options.homeDir || os.homedir();
|
|
337
|
+
const config = readConfig(homeDir);
|
|
338
|
+
if (!config || !config.storage_repo) {
|
|
339
|
+
return { error: "not_configured" };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const { storage_repo } = config;
|
|
343
|
+
const key = args[0];
|
|
344
|
+
|
|
345
|
+
if (!key) {
|
|
346
|
+
return { error: "missing_key" };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const secretsPath = path.join(storage_repo, "donna", "secrets.md");
|
|
350
|
+
if (!fs.existsSync(secretsPath)) {
|
|
351
|
+
return { error: "secrets_not_found", key };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const content = fs.readFileSync(secretsPath, "utf8");
|
|
355
|
+
|
|
356
|
+
// Parse "- KEY: value" pattern (allowing optional spaces)
|
|
357
|
+
const pattern = new RegExp(`^-\\s+${key}:\\s*(.+)$`, "m");
|
|
358
|
+
const match = content.match(pattern);
|
|
359
|
+
|
|
360
|
+
if (!match) {
|
|
361
|
+
return { error: "key_not_found", key };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const value = match[1].trim();
|
|
365
|
+
|
|
366
|
+
// Check for placeholder patterns
|
|
367
|
+
if (
|
|
368
|
+
/^your-.+-here$/i.test(value) ||
|
|
369
|
+
value === "TODO" ||
|
|
370
|
+
value === "PLACEHOLDER" ||
|
|
371
|
+
value === "xxx" ||
|
|
372
|
+
/^<.+>$/.test(value)
|
|
373
|
+
) {
|
|
374
|
+
return { error: "placeholder_value", key, value };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return { key, value };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
381
|
+
// CLI entry point
|
|
382
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Main CLI entry point. Routes subcommands and prints JSON to stdout.
|
|
386
|
+
*/
|
|
387
|
+
async function main() {
|
|
388
|
+
const args = process.argv.slice(2);
|
|
389
|
+
const cmd = args[0];
|
|
390
|
+
|
|
391
|
+
if (cmd === "init") {
|
|
392
|
+
const result = await runInit(args.slice(1));
|
|
393
|
+
console.log(JSON.stringify(result));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (cmd === "commit") {
|
|
398
|
+
const result = await runCommit(args.slice(1));
|
|
399
|
+
console.log(JSON.stringify(result));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (cmd === "daily-path") {
|
|
404
|
+
const result = await runDailyPath(args.slice(1));
|
|
405
|
+
console.log(JSON.stringify(result));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (cmd === "resolve-secret") {
|
|
410
|
+
const result = await runResolveSecret(args.slice(1));
|
|
411
|
+
console.log(JSON.stringify(result));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
process.stderr.write(`Unknown command: ${cmd}\n`);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
main.catch = undefined; // prevent accidental chaining — the module handles it below
|
|
420
|
+
|
|
421
|
+
module.exports = { main, runInit, runCommit, runDailyPath, runResolveSecret };
|
|
422
|
+
|
|
423
|
+
// Only run main when invoked directly (not when require()'d in tests)
|
|
424
|
+
if (require.main === module) {
|
|
425
|
+
main().catch((err) => {
|
|
426
|
+
process.stderr.write(`${err.message}\n`);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
});
|
|
429
|
+
}
|
package/src/installer.cjs
CHANGED
|
@@ -79,7 +79,7 @@ async function run(options = {}) {
|
|
|
79
79
|
for (const provider of detected) {
|
|
80
80
|
fs.cpSync(provider.stubSource, provider.stubTarget, { recursive: true });
|
|
81
81
|
output.success(
|
|
82
|
-
`Copied donna skills (setup, add-task, done, set-role, begin-the-day, add-tool, relearn-tools, run-tools, help, contribute-idea, adjust-tool, focus) to ${provider.stubTarget}`,
|
|
82
|
+
`Copied donna skills (setup, add-task, done, set-role, begin-the-day, add-tool, remove-tool, relearn-tools, run-tools, help, contribute-idea, adjust-tool, focus) to ${provider.stubTarget}`,
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
85
|
} else {
|
|
@@ -93,13 +93,35 @@ async function run(options = {}) {
|
|
|
93
93
|
fs.cpSync(workflowsSource, workflowsTarget, { recursive: true });
|
|
94
94
|
output.success("Installed workflows to ~/.donna/workflows/");
|
|
95
95
|
|
|
96
|
+
// Copy donna-tools.cjs and its dependencies to donnaDir for workflow access
|
|
97
|
+
const toolFiles = [
|
|
98
|
+
"donna-tools.cjs",
|
|
99
|
+
"version.cjs",
|
|
100
|
+
"migrator.cjs",
|
|
101
|
+
"changelog.cjs",
|
|
102
|
+
"output.cjs",
|
|
103
|
+
];
|
|
104
|
+
for (const file of toolFiles) {
|
|
105
|
+
const src = path.join(__dirname, file);
|
|
106
|
+
if (fs.existsSync(src)) {
|
|
107
|
+
fs.copyFileSync(src, path.join(donnaDir, file));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
output.success("Installed donna-tools.cjs + dependencies to ~/.donna/");
|
|
111
|
+
|
|
96
112
|
// Write version.md
|
|
97
113
|
version.writeVersion(donnaDir, packageVersion, lastSuccessful);
|
|
98
114
|
output.success(`Version ${packageVersion} installed`);
|
|
99
115
|
|
|
100
|
-
// Final message
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
// Final message — suppress setup prompt if already configured (D-04)
|
|
117
|
+
const configPath = path.join(homeDir, ".config", "donna", "config.md");
|
|
118
|
+
const isConfigured =
|
|
119
|
+
fs.existsSync(configPath) && fs.readFileSync(configPath, "utf8").includes("storage_repo:");
|
|
120
|
+
|
|
121
|
+
if (!isConfigured) {
|
|
122
|
+
console.log("");
|
|
123
|
+
output.info("Run /donna:setup in Claude Code to get started.");
|
|
124
|
+
}
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
module.exports = { run };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: donna:remove-tool
|
|
3
|
+
description: Remove a registered tool from tools.md
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Write
|
|
7
|
+
- Bash
|
|
8
|
+
- AskUserQuestion
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<objective>
|
|
12
|
+
Run the Donna remove-tool workflow. Remove a registered tool cleanly.
|
|
13
|
+
</objective>
|
|
14
|
+
|
|
15
|
+
<execution_context>
|
|
16
|
+
@~/.donna/workflows/remove-tool.md
|
|
17
|
+
</execution_context>
|
package/workflows/add-task.md
CHANGED
|
@@ -4,62 +4,25 @@
|
|
|
4
4
|
Capture a new task to today's daily journal file in the storage repo and commit it to git.
|
|
5
5
|
</objective>
|
|
6
6
|
|
|
7
|
-
<step name="
|
|
8
|
-
Read `~/.config/donna/config.md`.
|
|
9
|
-
|
|
10
|
-
If the file does not exist, print:
|
|
11
|
-
```
|
|
12
|
-
✗ Donna is not configured. Run /donna:setup first.
|
|
13
|
-
```
|
|
14
|
-
Stop.
|
|
15
|
-
|
|
16
|
-
Extract the `storage_repo`, `daily_folder` (default: `daily`), and `auto_push` (default: false) fields from the YAML frontmatter.
|
|
17
|
-
|
|
18
|
-
**Obsidian sync:** Check if `<storage_repo>/.obsidian/daily-notes.json` exists.
|
|
19
|
-
- If it exists and has a `folder` field that differs from `<daily_folder>`: update `<daily_folder>` to match Obsidian's value, and update `~/.config/donna/config.md` with the new `daily_folder`. Print `✓ Synced daily folder with Obsidian: <daily_folder>`.
|
|
20
|
-
- If `<storage_repo>/.obsidian/` exists but `daily-notes.json` does not exist or has no `folder` field: write `<storage_repo>/.obsidian/daily-notes.json` with `{"folder":"<daily_folder>"}`. Print `✓ Configured Obsidian daily notes to use <daily_folder>/`.
|
|
21
|
-
- Otherwise: do nothing.
|
|
22
|
-
</step>
|
|
23
|
-
|
|
24
|
-
<step name="check-pending-migrations">
|
|
25
|
-
Read `~/.donna/state.md` with the Read tool. If the file does not exist or has no `pending_migrations` field in its YAML frontmatter, skip this step.
|
|
26
|
-
|
|
27
|
-
For each entry in `pending_migrations`:
|
|
28
|
-
|
|
29
|
-
**`move-standing-files`:** Move standing files from storage repo root to donna/ subfolder.
|
|
30
|
-
|
|
7
|
+
<step name="init">
|
|
31
8
|
Run via Bash:
|
|
32
9
|
```bash
|
|
33
|
-
|
|
34
|
-
DONNA_DIR="$STORAGE_REPO/donna"
|
|
35
|
-
MOVED=0
|
|
36
|
-
|
|
37
|
-
mkdir -p "$DONNA_DIR"
|
|
38
|
-
for FILE in role.md recurring.md role-research.md; do
|
|
39
|
-
if [ -f "$STORAGE_REPO/$FILE" ] && [ ! -f "$DONNA_DIR/$FILE" ]; then
|
|
40
|
-
mv "$STORAGE_REPO/$FILE" "$DONNA_DIR/$FILE"
|
|
41
|
-
echo "Moved $FILE to donna/$FILE"
|
|
42
|
-
MOVED=$((MOVED + 1))
|
|
43
|
-
fi
|
|
44
|
-
done
|
|
45
|
-
|
|
46
|
-
echo "MOVED=$MOVED"
|
|
10
|
+
INIT=$(node ~/.donna/donna-tools.cjs init)
|
|
47
11
|
```
|
|
48
12
|
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
git -C <storage_repo> add -A
|
|
52
|
-
git -C <storage_repo> diff --cached --quiet || git -C <storage_repo> commit -m "donna(migrate): move standing files to donna/ subfolder"
|
|
13
|
+
Parse the JSON response. If the `error` field is `"not_configured"`, print:
|
|
53
14
|
```
|
|
15
|
+
x Donna is not configured. Run /donna:setup first.
|
|
16
|
+
```
|
|
17
|
+
Stop.
|
|
54
18
|
|
|
55
|
-
|
|
19
|
+
Extract `storage_repo`, `daily_folder`, `auto_push` from the JSON.
|
|
56
20
|
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
pending_migrations: []
|
|
61
|
-
---
|
|
21
|
+
If `update_available` is non-null, print:
|
|
22
|
+
```
|
|
23
|
+
Donna v<update_available> available -- run npx @pingvinen/donna-assistant to update
|
|
62
24
|
```
|
|
25
|
+
Continue normally.
|
|
63
26
|
</step>
|
|
64
27
|
|
|
65
28
|
<step name="get-description">
|
|
@@ -74,17 +37,11 @@ Store the response as `<description>`.
|
|
|
74
37
|
</step>
|
|
75
38
|
|
|
76
39
|
<step name="ensure-daily-file">
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
date +%Y-%m-%d
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Store the result as `<date>`. Construct the daily file path: `<storage_repo>/<daily_folder>/<date>.md`.
|
|
83
|
-
|
|
84
|
-
Run via Bash to ensure the daily folder exists:
|
|
40
|
+
Get the daily file path via donna-tools:
|
|
85
41
|
```bash
|
|
86
|
-
|
|
42
|
+
DAILY_PATH=$(node ~/.donna/donna-tools.cjs daily-path | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).path))")
|
|
87
43
|
```
|
|
44
|
+
Store the result as `<daily_file_path>`. Extract `<date>` from the filename (last path component without `.md`).
|
|
88
45
|
|
|
89
46
|
If the daily file does not exist, create it with the Write tool using this content (substituting the actual date):
|
|
90
47
|
```markdown
|
|
@@ -107,24 +64,7 @@ Write the updated file with the Write tool.
|
|
|
107
64
|
<step name="git-commit">
|
|
108
65
|
Run via Bash:
|
|
109
66
|
```bash
|
|
110
|
-
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Check whether there is anything to commit:
|
|
114
|
-
```bash
|
|
115
|
-
git -C <storage_repo> status --porcelain
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
If the output is empty, skip the commit and continue.
|
|
119
|
-
|
|
120
|
-
Otherwise, run:
|
|
121
|
-
```bash
|
|
122
|
-
git -C <storage_repo> commit -m "donna(add-task): <description>"
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
If `auto_push` is true in config, also run:
|
|
126
|
-
```bash
|
|
127
|
-
git -C <storage_repo> push
|
|
67
|
+
node ~/.donna/donna-tools.cjs commit "donna(task): added <description>" --files <daily_folder>/<date>.md
|
|
128
68
|
```
|
|
129
69
|
</step>
|
|
130
70
|
|