@pingvinen/donna-assistant 0.9.1 → 0.11.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 +33 -3
- package/package.json +1 -1
- package/src/donna-tools.cjs +429 -0
- package/src/installer.cjs +26 -4
- package/stubs/claude-code/donna/add-follow-up-task.md +17 -0
- 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 +59 -92
- package/workflows/done.md +16 -74
- package/workflows/focus.md +21 -95
- package/workflows/follow-up.md +163 -0
- 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
|
|
@@ -111,6 +111,7 @@ If a CLI tool version is newer than what the AI model knows, Donna will re-learn
|
|
|
111
111
|
donna/
|
|
112
112
|
role.md # your job role definition
|
|
113
113
|
recurring.md # recurring task schedule
|
|
114
|
+
follow-ups.md # scheduled follow-up tasks with due dates
|
|
114
115
|
tools.md # registered tool configurations
|
|
115
116
|
daily/
|
|
116
117
|
2026-03-16.md # today's tasks, tool data, notes
|
|
@@ -126,22 +127,51 @@ The storage repo is yours. Donna only writes to the `donna/` and `daily/` direct
|
|
|
126
127
|
|
|
127
128
|
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
129
|
|
|
130
|
+
## Why not automate tool pulls?
|
|
131
|
+
|
|
132
|
+
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:
|
|
133
|
+
|
|
134
|
+
- **Cost** -- every invocation consumes API tokens. A cron job running hourly would burn tokens even when you are not working.
|
|
135
|
+
- **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.
|
|
136
|
+
- **Conflicts** -- background runs could write to your daily file while you are editing it, causing git merge conflicts in your storage repo.
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
129
140
|
## All commands
|
|
130
141
|
|
|
142
|
+
### Setup and configuration
|
|
143
|
+
|
|
131
144
|
| Command | What it does |
|
|
132
145
|
|---------|-------------|
|
|
133
146
|
| `/donna:setup` | First-time configuration (storage repo, directories) |
|
|
134
147
|
| `/donna:set-role` | Define your job role, get recurring task suggestions |
|
|
148
|
+
|
|
149
|
+
### Daily workflow
|
|
150
|
+
|
|
151
|
+
| Command | What it does |
|
|
152
|
+
|---------|-------------|
|
|
135
153
|
| `/donna:begin-the-day` | Morning brief with carry-forward, recurring tasks, tool data |
|
|
136
154
|
| `/donna:add-task` | Capture a task instantly |
|
|
137
155
|
| `/donna:done` | Mark a task complete |
|
|
156
|
+
| `/donna:add-follow-up-task` | Schedule a task for a future date -- Donna reminds you when it is due |
|
|
157
|
+
| `/donna:focus` | Distill today's tasks into a short prioritized focus list |
|
|
158
|
+
|
|
159
|
+
### Tool management
|
|
160
|
+
|
|
161
|
+
| Command | What it does |
|
|
162
|
+
|---------|-------------|
|
|
138
163
|
| `/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
|
|
164
|
+
| `/donna:adjust-tool` | Edit a tool's configuration (scope, capabilities, auth, command) |
|
|
165
|
+
| `/donna:remove-tool` | Remove a registered tool from tools.md |
|
|
140
166
|
| `/donna:run-tools` | Refresh tool data mid-day |
|
|
141
167
|
| `/donna:relearn-tools` | Update CLI tool knowledge after upgrades |
|
|
168
|
+
|
|
169
|
+
### Help and feedback
|
|
170
|
+
|
|
171
|
+
| Command | What it does |
|
|
172
|
+
|---------|-------------|
|
|
142
173
|
| `/donna:help` | Conversational troubleshooting for config, storage, or skill issues |
|
|
143
174
|
| `/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
175
|
|
|
146
176
|
## License
|
|
147
177
|
|
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, add-follow-up-task) 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:add-follow-up-task
|
|
3
|
+
description: Schedule a follow-up task for a future date
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Write
|
|
7
|
+
- Bash
|
|
8
|
+
- AskUserQuestion
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<objective>
|
|
12
|
+
Run the Donna follow-up workflow.
|
|
13
|
+
</objective>
|
|
14
|
+
|
|
15
|
+
<execution_context>
|
|
16
|
+
@~/.donna/workflows/follow-up.md
|
|
17
|
+
</execution_context>
|
|
@@ -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>
|