@cccarv82/freya 1.0.3 → 1.0.5

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
@@ -58,6 +58,23 @@ freya init meu-projeto # cria ./meu-projeto
58
58
  freya init --here # instala no diretório atual
59
59
  ```
60
60
 
61
+ ### Atualizar uma workspace existente (sem perder dados)
62
+ Por padrão, ao rodar `init` em uma pasta existente, o CLI **preserva**:
63
+ - `data/**`
64
+ - `logs/**`
65
+
66
+ E atualiza/instala normalmente:
67
+ - `.agent/**`
68
+ - `scripts/**`
69
+ - `README.md`, `USER_GUIDE.md`
70
+ - `package.json` (merge de scripts)
71
+
72
+ Flags (use com cuidado):
73
+ ```bash
74
+ freya init --here --force-data # permite sobrescrever data/
75
+ freya init --here --force-logs # permite sobrescrever logs/
76
+ ```
77
+
61
78
  ## 🚀 Como Usar
62
79
 
63
80
  1. Abra a pasta da workspace gerada (ex.: `./freya`) na **sua IDE**.
package/cli/index.js CHANGED
@@ -3,39 +3,58 @@
3
3
  const path = require('path');
4
4
 
5
5
  const { cmdInit } = require('./init');
6
+ const { cmdWeb } = require('./web');
6
7
 
7
8
  function usage() {
8
9
  return `
9
10
  freya - F.R.E.Y.A. CLI
10
11
 
11
12
  Usage:
12
- freya init [dir] [--force] [--here|--in-place]
13
+ freya init [dir] [--force] [--here|--in-place] [--force-data] [--force-logs]
14
+ freya web [--port <n>] [--dir <path>] [--no-open]
13
15
 
14
16
  Defaults:
15
17
  - If no [dir] is provided, creates ./freya
18
+ - Preserves existing data/ and logs/ by default
16
19
 
17
20
  Examples:
18
21
  freya init # creates ./freya
19
22
  freya init my-workspace # creates ./my-workspace
20
23
  freya init --here # installs into current directory
24
+ freya init --here --force # update agents/scripts, keep data/logs
25
+ freya init --here --force-data # overwrite data/ too (danger)
21
26
  npx @cccarv82/freya init
27
+
28
+ freya web
29
+ freya web --dir ./freya --port 3872
22
30
  `;
23
31
  }
24
32
 
25
33
  function parseArgs(argv) {
26
34
  const args = [];
27
35
  const flags = new Set();
36
+ const kv = {};
28
37
 
29
- for (const a of argv) {
30
- if (a.startsWith('--')) flags.add(a);
31
- else args.push(a);
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const a = argv[i];
40
+ if (a.startsWith('--')) {
41
+ const next = argv[i + 1];
42
+ if (next && !next.startsWith('--')) {
43
+ kv[a] = next;
44
+ i++;
45
+ } else {
46
+ flags.add(a);
47
+ }
48
+ } else {
49
+ args.push(a);
50
+ }
32
51
  }
33
52
 
34
- return { args, flags };
53
+ return { args, flags, kv };
35
54
  }
36
55
 
37
56
  async function run(argv) {
38
- const { args, flags } = parseArgs(argv);
57
+ const { args, flags, kv } = parseArgs(argv);
39
58
  const command = args[0];
40
59
 
41
60
  if (!command || command === 'help' || flags.has('--help') || flags.has('-h')) {
@@ -49,8 +68,27 @@ async function run(argv) {
49
68
  const targetDir = args[1]
50
69
  ? path.resolve(process.cwd(), args[1])
51
70
  : (inPlace ? process.cwd() : defaultDir);
71
+
52
72
  const force = flags.has('--force');
53
- await cmdInit({ targetDir, force });
73
+ const forceData = flags.has('--force-data');
74
+ const forceLogs = flags.has('--force-logs');
75
+
76
+ await cmdInit({ targetDir, force, forceData, forceLogs });
77
+ return;
78
+ }
79
+
80
+ if (command === 'web') {
81
+ const port = Number(kv['--port'] || 3872);
82
+ const dir = kv['--dir'] ? path.resolve(process.cwd(), kv['--dir']) : null;
83
+ const open = !flags.has('--no-open');
84
+
85
+ if (!Number.isFinite(port) || port <= 0) {
86
+ process.stderr.write('Invalid --port\n');
87
+ process.exitCode = 1;
88
+ return;
89
+ }
90
+
91
+ await cmdWeb({ port, dir, open });
54
92
  return;
55
93
  }
56
94
 
package/cli/init.js CHANGED
@@ -36,14 +36,35 @@ function copyFile(src, dest, force) {
36
36
  return { copied: true };
37
37
  }
38
38
 
39
- function copyDirRecursive(srcDir, destDir, force, summary) {
39
+ function isNonEmptyDir(dir) {
40
+ try {
41
+ const entries = fs.readdirSync(dir);
42
+ // ignore common empty markers
43
+ const meaningful = entries.filter((e) => e !== '.DS_Store');
44
+ return meaningful.length > 0;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ function copyDirRecursive(srcDir, destDir, force, summary, options = {}) {
40
51
  const entries = fs.readdirSync(srcDir, { withFileTypes: true });
41
52
  for (const ent of entries) {
42
53
  const src = path.join(srcDir, ent.name);
43
54
  const dest = path.join(destDir, ent.name);
44
55
 
56
+ // Preserve user state by default
45
57
  if (ent.isDirectory()) {
46
- copyDirRecursive(src, dest, force, summary);
58
+ if (ent.name === 'data' && isNonEmptyDir(dest) && !options.forceData) {
59
+ summary.skipped.push('data/**');
60
+ continue;
61
+ }
62
+ if (ent.name === 'logs' && isNonEmptyDir(dest) && !options.forceLogs) {
63
+ summary.skipped.push('logs/**');
64
+ continue;
65
+ }
66
+
67
+ copyDirRecursive(src, dest, force, summary, options);
47
68
  continue;
48
69
  }
49
70
 
@@ -97,7 +118,7 @@ function ensurePackageJson(targetDir, force, summary) {
97
118
  summary.updated.push('package.json');
98
119
  }
99
120
 
100
- async function cmdInit({ targetDir, force }) {
121
+ async function cmdInit({ targetDir, force, forceData = false, forceLogs = false }) {
101
122
  const templateDir = path.join(__dirname, '..', 'templates', 'base');
102
123
  if (!exists(templateDir)) throw new Error(`Missing template directory: ${templateDir}`);
103
124
 
@@ -105,8 +126,8 @@ async function cmdInit({ targetDir, force }) {
105
126
 
106
127
  const summary = { copied: [], created: [], updated: [], skipped: [] };
107
128
 
108
- // Copy template files
109
- copyDirRecursive(templateDir, targetDir, force, summary);
129
+ // Copy template files (preserve data/logs by default)
130
+ copyDirRecursive(templateDir, targetDir, force, summary, { forceData, forceLogs });
110
131
 
111
132
  // Ensure package.json has scripts
112
133
  ensurePackageJson(targetDir, force, summary);
package/cli/web.js ADDED
@@ -0,0 +1,381 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawn } = require('child_process');
7
+
8
+ function guessNpmCmd() {
9
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
10
+ }
11
+
12
+ function guessOpenCmd() {
13
+ // Minimal cross-platform opener without extra deps
14
+ if (process.platform === 'win32') return { cmd: 'cmd', args: ['/c', 'start', ''] };
15
+ if (process.platform === 'darwin') return { cmd: 'open', args: [] };
16
+ return { cmd: 'xdg-open', args: [] };
17
+ }
18
+
19
+ function exists(p) {
20
+ try { fs.accessSync(p); return true; } catch { return false; }
21
+ }
22
+
23
+ function newestFile(dir, prefix) {
24
+ if (!exists(dir)) return null;
25
+ const files = fs.readdirSync(dir)
26
+ .filter((f) => f.startsWith(prefix) && f.endsWith('.md'))
27
+ .map((f) => ({ f, p: path.join(dir, f) }))
28
+ .filter((x) => {
29
+ try { return fs.statSync(x.p).isFile(); } catch { return false; }
30
+ })
31
+ .sort((a, b) => {
32
+ try { return fs.statSync(b.p).mtimeMs - fs.statSync(a.p).mtimeMs; } catch { return 0; }
33
+ });
34
+ return files[0]?.p || null;
35
+ }
36
+
37
+ function safeJson(res, code, obj) {
38
+ const body = JSON.stringify(obj);
39
+ res.writeHead(code, {
40
+ 'Content-Type': 'application/json; charset=utf-8',
41
+ 'Cache-Control': 'no-store',
42
+ 'Content-Length': Buffer.byteLength(body)
43
+ });
44
+ res.end(body);
45
+ }
46
+
47
+ function readBody(req) {
48
+ return new Promise((resolve, reject) => {
49
+ const chunks = [];
50
+ req.on('data', (c) => chunks.push(c));
51
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
52
+ req.on('error', reject);
53
+ });
54
+ }
55
+
56
+ function run(cmd, args, cwd) {
57
+ return new Promise((resolve) => {
58
+ const child = spawn(cmd, args, { cwd, shell: false, env: process.env });
59
+ let stdout = '';
60
+ let stderr = '';
61
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
62
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
63
+ child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
64
+ });
65
+ }
66
+
67
+ function openBrowser(url) {
68
+ const { cmd, args } = guessOpenCmd();
69
+ try {
70
+ spawn(cmd, [...args, url], { detached: true, stdio: 'ignore' }).unref();
71
+ } catch {
72
+ // ignore
73
+ }
74
+ }
75
+
76
+ function html() {
77
+ return `<!doctype html>
78
+ <html>
79
+ <head>
80
+ <meta charset="utf-8" />
81
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
82
+ <title>FREYA</title>
83
+ <style>
84
+ body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Helvetica, Arial; margin: 0; background: #0b0f14; color: #e6edf3; }
85
+ header { padding: 16px 18px; border-bottom: 1px solid #1f2a37; display:flex; gap:12px; align-items:center; }
86
+ header h1 { font-size: 14px; margin:0; letter-spacing: .08em; text-transform: uppercase; color:#9fb2c7; }
87
+ .wrap { max-width: 980px; margin: 0 auto; padding: 18px; }
88
+ .card { border: 1px solid #1f2a37; border-radius: 12px; background: #0f1620; padding: 14px; }
89
+ .grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
90
+ @media (min-width: 920px) { .grid { grid-template-columns: 1.2fr .8fr; } }
91
+ label { font-size: 12px; color:#9fb2c7; display:block; margin-bottom: 6px; }
92
+ input { width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid #223041; background:#0b1220; color:#e6edf3; }
93
+ .btns { display:flex; flex-wrap:wrap; gap: 10px; margin-top: 10px; }
94
+ button { padding: 10px 12px; border-radius: 10px; border: 1px solid #223041; background:#1f6feb; color:white; cursor:pointer; }
95
+ button.secondary { background: transparent; color:#e6edf3; }
96
+ button.danger { background: #d73a49; }
97
+ .row { display:grid; grid-template-columns: 1fr; gap: 10px; }
98
+ .small { font-size: 12px; color:#9fb2c7; }
99
+ pre { white-space: pre-wrap; background:#0b1220; border: 1px solid #223041; border-radius: 12px; padding: 12px; overflow:auto; }
100
+ a { color:#58a6ff; }
101
+ </style>
102
+ </head>
103
+ <body>
104
+ <header>
105
+ <h1>FREYA • Local-first Status Assistant</h1>
106
+ <span class="small" id="status"></span>
107
+ </header>
108
+ <div class="wrap">
109
+ <div class="grid">
110
+ <div class="card">
111
+ <div class="row">
112
+ <div>
113
+ <label>Workspace dir</label>
114
+ <input id="dir" placeholder="/path/to/freya (or ./freya)" />
115
+ <div class="small">Dica: a workspace é a pasta que contém <code>data/</code>, <code>logs/</code>, <code>scripts/</code>.</div>
116
+ </div>
117
+ <div class="btns">
118
+ <button onclick="doInit()">Init (preserva data/logs)</button>
119
+ <button class="secondary" onclick="doUpdate()">Update (init --here)</button>
120
+ <button class="secondary" onclick="doHealth()">Health</button>
121
+ <button class="secondary" onclick="doMigrate()">Migrate</button>
122
+ </div>
123
+ </div>
124
+
125
+ <hr style="border:0;border-top:1px solid #1f2a37;margin:14px 0" />
126
+
127
+ <div class="btns">
128
+ <button onclick="runReport('status')">Generate Executive Report</button>
129
+ <button onclick="runReport('sm-weekly')">Generate SM Weekly</button>
130
+ <button onclick="runReport('blockers')">Generate Blockers</button>
131
+ <button onclick="runReport('daily')">Generate Daily</button>
132
+ </div>
133
+
134
+ <div style="margin-top:12px">
135
+ <label>Output</label>
136
+ <pre id="out"></pre>
137
+ <div class="small" id="last"></div>
138
+ </div>
139
+ </div>
140
+
141
+ <div class="card">
142
+ <div class="row">
143
+ <div>
144
+ <label>Discord Webhook URL (optional)</label>
145
+ <input id="discord" placeholder="https://discord.com/api/webhooks/..." />
146
+ </div>
147
+ <div>
148
+ <label>Teams Webhook URL (optional)</label>
149
+ <input id="teams" placeholder="https://..." />
150
+ </div>
151
+ <div class="btns">
152
+ <button class="secondary" onclick="publish('discord')">Publish last → Discord</button>
153
+ <button class="secondary" onclick="publish('teams')">Publish last → Teams</button>
154
+ </div>
155
+ <div class="small">
156
+ Publica o último relatório gerado (cache local). Limite: ~1800 chars (pra evitar limites de webhook).
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ </div>
162
+ </div>
163
+
164
+ <script>
165
+ const $ = (id) => document.getElementById(id);
166
+ const state = {
167
+ lastReportPath: null,
168
+ lastText: ''
169
+ };
170
+
171
+ function setOut(text) {
172
+ state.lastText = text;
173
+ $('out').textContent = text || '';
174
+ }
175
+
176
+ function setLast(path) {
177
+ state.lastReportPath = path;
178
+ $('last').textContent = path ? ('Last report: ' + path) : '';
179
+ }
180
+
181
+ function saveLocal() {
182
+ localStorage.setItem('freya.dir', $('dir').value);
183
+ localStorage.setItem('freya.discord', $('discord').value);
184
+ localStorage.setItem('freya.teams', $('teams').value);
185
+ }
186
+
187
+ function loadLocal() {
188
+ $('dir').value = localStorage.getItem('freya.dir') || '';
189
+ $('discord').value = localStorage.getItem('freya.discord') || '';
190
+ $('teams').value = localStorage.getItem('freya.teams') || '';
191
+ }
192
+
193
+ async function api(path, body) {
194
+ const res = await fetch(path, {
195
+ method: body ? 'POST' : 'GET',
196
+ headers: body ? { 'Content-Type': 'application/json' } : {},
197
+ body: body ? JSON.stringify(body) : undefined
198
+ });
199
+ const json = await res.json();
200
+ if (!res.ok) throw new Error(json.error || 'Request failed');
201
+ return json;
202
+ }
203
+
204
+ function dirOrDefault() {
205
+ const d = $('dir').value.trim();
206
+ return d || './freya';
207
+ }
208
+
209
+ async function doInit() {
210
+ saveLocal();
211
+ setOut('Running init...');
212
+ const r = await api('/api/init', { dir: dirOrDefault() });
213
+ setOut(r.output);
214
+ setLast(null);
215
+ }
216
+
217
+ async function doUpdate() {
218
+ saveLocal();
219
+ setOut('Running update...');
220
+ const r = await api('/api/update', { dir: dirOrDefault() });
221
+ setOut(r.output);
222
+ setLast(null);
223
+ }
224
+
225
+ async function doHealth() {
226
+ saveLocal();
227
+ setOut('Running health...');
228
+ const r = await api('/api/health', { dir: dirOrDefault() });
229
+ setOut(r.output);
230
+ setLast(null);
231
+ }
232
+
233
+ async function doMigrate() {
234
+ saveLocal();
235
+ setOut('Running migrate...');
236
+ const r = await api('/api/migrate', { dir: dirOrDefault() });
237
+ setOut(r.output);
238
+ setLast(null);
239
+ }
240
+
241
+ async function runReport(name) {
242
+ saveLocal();
243
+ setOut('Running ' + name + '...');
244
+ const r = await api('/api/report', { dir: dirOrDefault(), script: name });
245
+ setOut(r.output);
246
+ setLast(r.reportPath || null);
247
+ }
248
+
249
+ async function publish(target) {
250
+ saveLocal();
251
+ if (!state.lastText) throw new Error('No cached output. Generate a report first.');
252
+ const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
253
+ setOut('Publishing to ' + target + '...');
254
+ const r = await api('/api/publish', { webhookUrl, text: state.lastText });
255
+ setOut('Published.');
256
+ }
257
+
258
+ loadLocal();
259
+ $('status').textContent = 'ready';
260
+ </script>
261
+ </body>
262
+ </html>`;
263
+ }
264
+
265
+ async function cmdWeb({ port, dir, open }) {
266
+ const host = '127.0.0.1';
267
+
268
+ const server = http.createServer(async (req, res) => {
269
+ try {
270
+ if (!req.url) return safeJson(res, 404, { error: 'Not found' });
271
+
272
+ if (req.method === 'GET' && req.url === '/') {
273
+ const body = html();
274
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
275
+ res.end(body);
276
+ return;
277
+ }
278
+
279
+ if (req.url.startsWith('/api/')) {
280
+ const raw = await readBody(req);
281
+ const payload = raw ? JSON.parse(raw) : {};
282
+
283
+ const workspaceDir = path.resolve(process.cwd(), payload.dir || dir || './freya');
284
+
285
+ if (req.url === '/api/init') {
286
+ const pkg = '@cccarv82/freya';
287
+ const r = await run('npx', [pkg, 'init', workspaceDir], process.cwd());
288
+ return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
289
+ }
290
+
291
+ if (req.url === '/api/update') {
292
+ const pkg = '@cccarv82/freya';
293
+ fs.mkdirSync(workspaceDir, { recursive: true });
294
+ const r = await run('npx', [pkg, 'init', '--here'], workspaceDir);
295
+ return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
296
+ }
297
+
298
+ const npmCmd = guessNpmCmd();
299
+
300
+ if (req.url === '/api/health') {
301
+ const r = await run(npmCmd, ['run', 'health'], workspaceDir);
302
+ return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
303
+ }
304
+
305
+ if (req.url === '/api/migrate') {
306
+ const r = await run(npmCmd, ['run', 'migrate'], workspaceDir);
307
+ return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
308
+ }
309
+
310
+ if (req.url === '/api/report') {
311
+ const script = payload.script;
312
+ if (!script) return safeJson(res, 400, { error: 'Missing script' });
313
+
314
+ const r = await run(npmCmd, ['run', script], workspaceDir);
315
+ const out = (r.stdout + r.stderr).trim();
316
+
317
+ const reportsDir = path.join(workspaceDir, 'docs', 'reports');
318
+ const prefixMap = {
319
+ blockers: 'blockers-',
320
+ 'sm-weekly': 'sm-weekly-',
321
+ status: 'executive-',
322
+ daily: null
323
+ };
324
+ const prefix = prefixMap[script] || null;
325
+ const reportPath = prefix ? newestFile(reportsDir, prefix) : null;
326
+
327
+ return safeJson(res, r.code === 0 ? 200 : 400, { output: out, reportPath });
328
+ }
329
+
330
+ if (req.url === '/api/publish') {
331
+ const webhookUrl = payload.webhookUrl;
332
+ const text = payload.text;
333
+ if (!webhookUrl) return safeJson(res, 400, { error: 'Missing webhookUrl' });
334
+ if (!text) return safeJson(res, 400, { error: 'Missing text' });
335
+
336
+ // Minimal webhook post: Discord expects {content}, Teams expects {text}
337
+ const u = new URL(webhookUrl);
338
+ const isDiscord = u.hostname.includes('discord.com') || u.hostname.includes('discordapp.com');
339
+ const body = JSON.stringify(isDiscord ? { content: text.slice(0, 1800) } : { text: text.slice(0, 1800) });
340
+
341
+ const options = {
342
+ method: 'POST',
343
+ hostname: u.hostname,
344
+ path: u.pathname + u.search,
345
+ headers: {
346
+ 'Content-Type': 'application/json',
347
+ 'Content-Length': Buffer.byteLength(body)
348
+ }
349
+ };
350
+
351
+ const req2 = require(u.protocol === 'https:' ? 'https' : 'http').request(options, (r2) => {
352
+ const chunks = [];
353
+ r2.on('data', (c) => chunks.push(c));
354
+ r2.on('end', () => {
355
+ if (r2.statusCode >= 200 && r2.statusCode < 300) return safeJson(res, 200, { ok: true });
356
+ return safeJson(res, 400, { error: `Webhook error ${r2.statusCode}: ${Buffer.concat(chunks).toString('utf8')}` });
357
+ });
358
+ });
359
+ req2.on('error', (e) => safeJson(res, 400, { error: e.message }));
360
+ req2.write(body);
361
+ req2.end();
362
+ return;
363
+ }
364
+
365
+ return safeJson(res, 404, { error: 'Not found' });
366
+ }
367
+
368
+ safeJson(res, 404, { error: 'Not found' });
369
+ } catch (e) {
370
+ safeJson(res, 500, { error: e.message || String(e) });
371
+ }
372
+ });
373
+
374
+ await new Promise((resolve) => server.listen(port, host, resolve));
375
+
376
+ const url = `http://${host}:${port}/`;
377
+ process.stdout.write(`FREYA web running at ${url}\n`);
378
+ if (open) openBrowser(url);
379
+ }
380
+
381
+ module.exports = { cmdWeb };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",
@@ -10,7 +10,7 @@
10
10
  "daily": "node scripts/generate-daily-summary.js",
11
11
  "status": "node scripts/generate-executive-report.js",
12
12
  "blockers": "node scripts/generate-blockers-report.js",
13
- "test": "node tests/unit/test-package-config.js && node tests/unit/test-cli-init.js && node tests/unit/test-fs-utils.js && node tests/unit/test-task-schema.js && node tests/unit/test-daily-generation.js && node tests/unit/test-report-generation.js && node tests/unit/test-oracle-retrieval.js && node tests/unit/test-task-completion.js && node tests/unit/test-migrate-data.js && node tests/unit/test-blockers-validation.js && node tests/unit/test-blockers-report.js && node tests/unit/test-sm-weekly-report.js && node tests/integration/test-ingestor-task.js"
13
+ "test": "node tests/unit/test-package-config.js && node tests/unit/test-cli-init.js && node tests/unit/test-cli-web-help.js && node tests/unit/test-fs-utils.js && node tests/unit/test-task-schema.js && node tests/unit/test-daily-generation.js && node tests/unit/test-report-generation.js && node tests/unit/test-oracle-retrieval.js && node tests/unit/test-task-completion.js && node tests/unit/test-migrate-data.js && node tests/unit/test-blockers-validation.js && node tests/unit/test-blockers-report.js && node tests/unit/test-sm-weekly-report.js && node tests/integration/test-ingestor-task.js"
14
14
  },
15
15
  "keywords": [],
16
16
  "author": "",