@pingvinen/donna-assistant 0.9.1 → 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 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, type) at any time:
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, type) |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pingvinen/donna-assistant",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Donna - your AI powered personal assistant",
5
5
  "bin": {
6
6
  "donna-assistant": "./bin/donna-assistant"
@@ -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
- console.log("");
102
- output.info("Run /donna:setup in Claude Code to get started.");
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>
@@ -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="read-config">
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
- STORAGE_REPO="<storage_repo>"
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
- If MOVED > 0, commit the move:
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
- If `auto_push` is true in config, also push.
19
+ Extract `storage_repo`, `daily_folder`, `auto_push` from the JSON.
56
20
 
57
- After processing all pending migrations, update `~/.donna/state.md` with the Write tool: remove the completed entries from `pending_migrations`. If no entries remain, write:
58
- ```markdown
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
- Run via Bash to get today's date:
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
- mkdir -p <storage_repo>/<daily_folder>
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
- git -C <storage_repo> add -A
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