create-pmos 0.203.0 → 0.205.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/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-capture-test.js +206 -0
- package/workspace/c3-report-design-real-test.js +7 -2
- package/workspace/extension/README.md +22 -7
- package/workspace/extension/capture.js +114 -0
- package/workspace/extension/manifest.json +49 -8
- package/workspace/extension/popup.css +28 -0
- package/workspace/extension/popup.html +15 -0
- package/workspace/extension/popup.js +195 -0
- package/workspace/guide/report-design.md +15 -1
- package/workspace/lib/extension-cli.js +6 -4
- package/workspace/lib/headless.js +9 -4
- package/workspace/lib/report-design.js +107 -17
- package/workspace/lib/routes-report-design.js +116 -5
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-boot.js +2 -1
- package/workspace/public/pm/c3-capture.css +23 -0
- package/workspace/public/pm/c3-capture.js +152 -0
- package/workspace/public/pm/c3-demo-rd.js +3 -1
- package/workspace/public/pm/c3-design-studio.js +20 -8
- package/workspace/public/pm/c3-shell.js +2 -0
- package/workspace/replaced-tests.json +14 -0
- package/workspace/skills/report-design.skill +2 -1
- package/workspace/skills/report-sample-fetch.skill +34 -0
- package/workspace/test-lists/report-design-studio-ui.json +3 -3
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* popup.js — the PMOS button's popup (popup.html): "Add this page to a report design".
|
|
2
|
+
* PMOS draws the popup: the page is one frame, ${address}/c3.html?capture=1&pop=1&…, which lists the reports and has the Add button. The extension never posts to PMOS
|
|
3
|
+
* itself, because every PMOS write needs a press on a PMOS page (a nonce and an Origin check). So when the frame asks (postMessage { pmosCapture: 'request' }), this
|
|
4
|
+
* page takes the open tab and posts it back to the frame ({ pmosCapture: 'data', … }), and the frame uploads it with its own press. Both ways, the message must
|
|
5
|
+
* come from the other window and from the PMOS origin; the extension posts to the PMOS origin only.
|
|
6
|
+
* Google Slides and Google Docs → the PDF, fetched here with the user's own Google cookies (host permission for https://docs.google.com/*)
|
|
7
|
+
* any other page → pictures of the whole page, scrolled top to bottom: at most 8, JPEG, 650 ms apart (Chrome's capture rate limit), scroll put back after
|
|
8
|
+
* When nothing answers at the address, this page says so and offers Try again. Nothing else is fetched. */
|
|
9
|
+
(function () {
|
|
10
|
+
'use strict';
|
|
11
|
+
var A = window.PMOS_ADDRESS;
|
|
12
|
+
var C = window.PMOS_CAPTURE;
|
|
13
|
+
var CHECK_MS = 4000; // how long to wait for PMOS to answer
|
|
14
|
+
var READY_MS = 8000; // how long to wait for the frame to say it is drawn
|
|
15
|
+
var SETTLE_MS = 300; // after a scroll, before the picture
|
|
16
|
+
var msg = document.getElementById('msg');
|
|
17
|
+
var frame = null;
|
|
18
|
+
var frameOrigin = '';
|
|
19
|
+
var readyTimer = null;
|
|
20
|
+
var busy = false;
|
|
21
|
+
var current = null; // { tab, kind, address }
|
|
22
|
+
|
|
23
|
+
function readAddress(done) {
|
|
24
|
+
var fallback = A.DEFAULT_ADDRESS;
|
|
25
|
+
try {
|
|
26
|
+
var o = {}; o[A.KEY] = fallback;
|
|
27
|
+
chrome.storage.sync.get(o, function (v) { done(A.normalizeAddress(v && v[A.KEY]) || fallback); });
|
|
28
|
+
} catch (e) { done(fallback); }
|
|
29
|
+
}
|
|
30
|
+
function getTab(done) {
|
|
31
|
+
try { chrome.tabs.query({ active: true, currentWindow: true }, function (t) { done((t && t[0]) || null); }); } catch (e) { done(null); }
|
|
32
|
+
}
|
|
33
|
+
/** Resolves when something answers at the address (an opaque no-cors reply is enough), rejects when nothing listens. */
|
|
34
|
+
function answers(addr) {
|
|
35
|
+
var ctl = typeof AbortController === 'function' ? new AbortController() : null;
|
|
36
|
+
var t = ctl ? setTimeout(function () { ctl.abort(); }, CHECK_MS) : null;
|
|
37
|
+
return fetch(addr + '/c3.html', { mode: 'no-cors', cache: 'no-store', signal: ctl ? ctl.signal : undefined })
|
|
38
|
+
.then(function (r) { if (t) clearTimeout(t); return r; }, function (e) { if (t) clearTimeout(t); throw e; });
|
|
39
|
+
}
|
|
40
|
+
var sleep = function (ms) { return new Promise(function (r) { setTimeout(r, ms); }); };
|
|
41
|
+
|
|
42
|
+
/* ── the popup's own states ── */
|
|
43
|
+
function head(host) {
|
|
44
|
+
var h = document.createElement('div'); h.className = 'head';
|
|
45
|
+
var b = document.createElement('b'); b.textContent = 'PMOS'; h.appendChild(b);
|
|
46
|
+
var s = document.createElement('span'); s.textContent = 'Add this page to a report design'; h.appendChild(s);
|
|
47
|
+
host.appendChild(h);
|
|
48
|
+
}
|
|
49
|
+
function show(text, withRetry, extra) {
|
|
50
|
+
if (frame) { frame.remove(); frame = null; }
|
|
51
|
+
clearTimeout(readyTimer);
|
|
52
|
+
msg.textContent = '';
|
|
53
|
+
msg.className = 'msg';
|
|
54
|
+
if (!msg.parentNode) document.body.appendChild(msg);
|
|
55
|
+
head(msg);
|
|
56
|
+
var p = document.createElement('p'); p.textContent = text; msg.appendChild(p);
|
|
57
|
+
if (extra) msg.appendChild(extra);
|
|
58
|
+
if (withRetry) {
|
|
59
|
+
var b = document.createElement('button'); b.type = 'button'; b.textContent = 'Try again'; b.setAttribute('data-act', 'retry');
|
|
60
|
+
b.addEventListener('click', function () { start(); });
|
|
61
|
+
msg.appendChild(b);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function showDown(addr) {
|
|
65
|
+
var a = document.createElement('a'); a.href = '#'; a.textContent = 'Set the address';
|
|
66
|
+
a.addEventListener('click', function (e) { e.preventDefault(); try { chrome.runtime.openOptionsPage(); } catch (x) { /* no options page */ } });
|
|
67
|
+
show('PMOS is not answering at ' + addr + '. Start PMOS, then try again.', true, a);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ── the frame ── */
|
|
71
|
+
function post(m) {
|
|
72
|
+
if (!frame || !frame.contentWindow || !C.validOutgoing(m)) return false;
|
|
73
|
+
frame.contentWindow.postMessage(m, frameOrigin);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
function showFrame(tab, kind, addr, screens) {
|
|
77
|
+
var q = new URLSearchParams({ capture: '1', pop: '1', pkind: kind, ptitle: C.sampleName(tab.title, tab.url), purl: tab.url || '' });
|
|
78
|
+
if (screens) q.set('pscreens', String(screens));
|
|
79
|
+
frame = document.createElement('iframe');
|
|
80
|
+
frame.title = 'PMOS: add this page to a report design';
|
|
81
|
+
frame.src = addr + '/c3.html?' + q.toString();
|
|
82
|
+
frameOrigin = new URL(addr).origin;
|
|
83
|
+
if (msg.parentNode) msg.remove();
|
|
84
|
+
document.body.appendChild(frame);
|
|
85
|
+
// an older PMOS answers but has no capture page: it never says ready
|
|
86
|
+
readyTimer = setTimeout(function () { showDown(addr); }, READY_MS);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── what is taken ── */
|
|
90
|
+
var toBase64 = function (buf) {
|
|
91
|
+
var bytes = new Uint8Array(buf); var s = '';
|
|
92
|
+
for (var i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
|
|
93
|
+
return btoa(s);
|
|
94
|
+
};
|
|
95
|
+
var run = function (tabId, func, args) {
|
|
96
|
+
return chrome.scripting.executeScript({ target: { tabId: tabId }, func: func, args: args || [] }).then(function (r) { return r && r[0] ? r[0].result : undefined; });
|
|
97
|
+
};
|
|
98
|
+
var measure = function () { var d = document.documentElement; var b = document.body; return { h: Math.max(d ? d.scrollHeight : 0, b ? b.scrollHeight : 0), vh: window.innerHeight, y: window.scrollY }; };
|
|
99
|
+
var scrollTo = function (y) { window.scrollTo({ top: y, left: 0, behavior: 'instant' }); return window.scrollY; };
|
|
100
|
+
|
|
101
|
+
function takePdf(tab, kind) {
|
|
102
|
+
var url = C.exportUrl(tab.url);
|
|
103
|
+
post({ pmosCapture: 'progress', text: 'Getting the PDF from Google', step: 1, of: 3 });
|
|
104
|
+
return fetch(url, { credentials: 'include', cache: 'no-store', redirect: 'follow' }).then(function (r) {
|
|
105
|
+
if (!r.ok) throw new Error('Google did not give the PDF (' + r.status + '). Open the file in Chrome while signed in to Google, then try again.');
|
|
106
|
+
return r.arrayBuffer();
|
|
107
|
+
}, function () { throw new Error('Chrome could not reach Google to get the PDF. Check the connection, then try again.'); }).then(function (buf) {
|
|
108
|
+
var bytes = new Uint8Array(buf);
|
|
109
|
+
if (!(bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46)) throw new Error('Google did not give a PDF. Sign in to Google in this Chrome, make sure you can open the file, then try again.');
|
|
110
|
+
if (bytes.length > C.MAX_BYTES) throw new Error('The PDF is over 20 MB. PMOS keeps a sample up to 20 MB. Add a smaller copy.');
|
|
111
|
+
post({ pmosCapture: 'progress', text: 'Reading the PDF', step: 2, of: 3 });
|
|
112
|
+
var pages = C.pdfPageCount(bytes);
|
|
113
|
+
return { pmosCapture: 'data', kind: 'pdf', name: C.sampleName(tab.title, tab.url), url: tab.url, pages: pages || undefined, data: toBase64(buf) };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function takePictures(tab) {
|
|
118
|
+
var tabId = tab.id; var windowId = tab.windowId;
|
|
119
|
+
var start = null; var pics = []; var bytes = 0; var last = 0;
|
|
120
|
+
return run(tabId, measure).then(function (m) {
|
|
121
|
+
if (!m || !m.vh) throw new Error('Chrome does not let PMOS read this page.');
|
|
122
|
+
start = m.y || 0;
|
|
123
|
+
var plan = C.scrollPlan(m.h, m.vh, C.MAX_PICTURES);
|
|
124
|
+
var n = plan.offsets.length;
|
|
125
|
+
var chain = Promise.resolve();
|
|
126
|
+
plan.offsets.forEach(function (y, i) {
|
|
127
|
+
chain = chain.then(function () {
|
|
128
|
+
post({ pmosCapture: 'progress', text: 'Picture ' + (i + 1) + ' of ' + n + (i ? ', scrolling down' : ''), step: i + 1, of: n + 1 });
|
|
129
|
+
return run(tabId, scrollTo, [y]);
|
|
130
|
+
}).then(function () {
|
|
131
|
+
var wait = Math.max(SETTLE_MS, C.MIN_GAP_MS - (Date.now() - last));
|
|
132
|
+
return sleep(wait);
|
|
133
|
+
}).then(function () {
|
|
134
|
+
last = Date.now();
|
|
135
|
+
return chrome.tabs.captureVisibleTab(windowId, { format: 'jpeg', quality: 80 });
|
|
136
|
+
}).then(function (dataUrl) {
|
|
137
|
+
var b64 = String(dataUrl || '').replace(/^data:[^,]*,/, '');
|
|
138
|
+
if (!b64) throw new Error('Chrome did not give a picture of the page.');
|
|
139
|
+
bytes += C.base64Bytes(b64);
|
|
140
|
+
if (bytes > C.MAX_BYTES) throw new Error('The pictures are over 20 MB. PMOS keeps a sample up to 20 MB. Open a shorter page, or add it as a PDF.');
|
|
141
|
+
pics.push(b64);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
return chain;
|
|
145
|
+
}).then(function () {
|
|
146
|
+
return { pmosCapture: 'data', kind: 'pictures', name: C.sampleName(tab.title, tab.url), url: tab.url, pictures: pics };
|
|
147
|
+
}).then(function (out) { return restore(tabId, start).then(function () { return out; }); },
|
|
148
|
+
function (e) { return restore(tabId, start).then(function () { throw e; }); });
|
|
149
|
+
}
|
|
150
|
+
function restore(tabId, y) { return y == null ? Promise.resolve() : run(tabId, scrollTo, [y]).catch(function () { /* the page went away */ }); }
|
|
151
|
+
|
|
152
|
+
function capture() {
|
|
153
|
+
if (busy || !current) return;
|
|
154
|
+
busy = true;
|
|
155
|
+
var tab = current.tab;
|
|
156
|
+
var job = C.exportUrl(tab.url) ? takePdf(tab, current.kind) : takePictures(tab);
|
|
157
|
+
job.then(function (out) {
|
|
158
|
+
post({ pmosCapture: 'progress', text: 'Sending it to PMOS', step: out.kind === 'pdf' ? 3 : (out.pictures.length + 1), of: out.kind === 'pdf' ? 3 : (out.pictures.length + 1) });
|
|
159
|
+
if (!post(out)) throw new Error('The page could not be sent to PMOS.');
|
|
160
|
+
}).catch(function (e) {
|
|
161
|
+
post({ pmosCapture: 'error', error: String((e && e.message) || 'The page could not be taken.') });
|
|
162
|
+
}).then(function () { busy = false; });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
window.addEventListener('message', function (e) {
|
|
166
|
+
var m = C.fromFrame(e, frame && frame.contentWindow, frameOrigin);
|
|
167
|
+
if (!m) return;
|
|
168
|
+
if (m.pmosCapture === 'ready') { clearTimeout(readyTimer); return; }
|
|
169
|
+
if (m.pmosCapture === 'resize') { if (frame) frame.style.height = m.height + 'px'; return; }
|
|
170
|
+
if (m.pmosCapture === 'close') { window.close(); return; }
|
|
171
|
+
if (m.pmosCapture === 'open') {
|
|
172
|
+
try { chrome.tabs.create({ url: frameOrigin + '/c3.html?studio=' + encodeURIComponent(m.report) }); } catch (x) { /* no tabs */ }
|
|
173
|
+
window.close();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (m.pmosCapture === 'request') capture();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
function start() {
|
|
180
|
+
busy = false;
|
|
181
|
+
readAddress(function (addr) {
|
|
182
|
+
getTab(function (tab) {
|
|
183
|
+
var kind = tab ? C.classifyUrl(tab.url) : 'blocked';
|
|
184
|
+
if (!tab || kind === 'blocked') { show('Chrome does not let PMOS read this page. Open a web page, such as a Google Slides deck or any site, then press the PMOS button.', false); return; }
|
|
185
|
+
current = { tab: tab, kind: kind, address: addr };
|
|
186
|
+
var go = function (screens) {
|
|
187
|
+
answers(addr).then(function () { showFrame(tab, C.pageKind(tab.url), addr, screens); }, function () { showDown(addr); });
|
|
188
|
+
};
|
|
189
|
+
if (kind !== 'other') { go(0); return; }
|
|
190
|
+
run(tab.id, measure).then(function (m) { go(m && m.vh ? C.scrollPlan(m.h, m.vh, 1000).total : 0); }, function () { go(0); });
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
start();
|
|
195
|
+
})();
|
|
@@ -22,7 +22,7 @@ Use one of these two buttons:
|
|
|
22
22
|
|
|
23
23
|
1. Select **Deck** or **Document** at the top of the studio.
|
|
24
24
|
2. Write what the report is for and who reads it. For example: "This goes to the progress session every other Thursday. The decisions must stand out."
|
|
25
|
-
3. Press **Add a sample** to give PMOS a report that people liked. You can upload a file, paste a link, or pick a report document that PMOS has. Each sample shows what PMOS took from it. PMOS
|
|
25
|
+
3. Press **Add a sample** to give PMOS a report that people liked. You can upload a file, paste a link, add a page from Chrome (see below), or pick a report document that PMOS has. Each sample shows what PMOS took from it. PMOS reads a link from Google Slides, Docs or Sheets, or from Confluence, through your own connectors. It saves the words and the structure (slide titles, order, sections and tables), and it changes nothing in the file. It does not get the look. To match the look, also add the file as a PDF or a screenshot. For Google Slides, choose **File**, **Download**, **PDF document (.pdf)** in Slides, then add that file. If your connectors cannot read the link, the sample says so. Any other link is kept by its address, and PMOS cannot open it. PMOS sees the pages of a PDF or an image, so it can follow their look. From a .pptx or a .docx, PMOS gets only the words.
|
|
26
26
|
4. Press **Make the next version**. PMOS makes a version from your words and this cycle's data. This takes a short time. You can leave the studio. The studio shows when the version is ready.
|
|
27
27
|
5. Read the version in the preview. Write what to change and press **Make the next version** again. Each turn makes a new version: D1, D2, D3 and so on.
|
|
28
28
|
|
|
@@ -30,6 +30,20 @@ To set the width of the studio, use **Half**, **3/4** or **Whole** at the top ri
|
|
|
30
30
|
|
|
31
31
|
If a turn fails, the studio shows why. Nothing changes. Try the turn again.
|
|
32
32
|
|
|
33
|
+
|
|
34
|
+
### Add a page from Chrome
|
|
35
|
+
|
|
36
|
+
You can add the page you are looking at in Chrome, on any website, as a sample. This needs the PMOS extension in Chrome. Run `pmos extension` in a terminal for the steps to load it.
|
|
37
|
+
|
|
38
|
+
1. In Chrome, open the page, for example a Google Slides deck, a Google Doc or a Confluence page.
|
|
39
|
+
2. Press the **PMOS** button in the Chrome toolbar. The popup says "Add this page to a report design".
|
|
40
|
+
3. Choose the report in the **Report** list. It starts on the report whose design studio you opened last.
|
|
41
|
+
4. Press **Add as a sample**. Keep the popup open while it saves. When it says "Added to" and the name of the report, press **Open the studio**.
|
|
42
|
+
|
|
43
|
+
A Google Slides deck or a Google Doc comes to PMOS as a PDF, so PMOS can follow its look. The sample reads "From Chrome, PDF, 12 slides", or "pages" for a document. Chrome asks Google for the PDF with your own Google sign-in. Any other page comes as pictures of the whole page, scrolled top to bottom, at most 8 pictures. The sample reads "From Chrome, 3 page pictures". PMOS reads every picture, and follows its look as it does for a PDF. A sample is at most 20 MB. If the page is bigger, the popup says so: open a shorter page, or add a smaller copy.
|
|
44
|
+
|
|
45
|
+
The sample shows in a design studio that is already open, in a few seconds. If the popup says "PMOS is not answering", start PMOS and press **Try again**. Nothing leaves your computer: the popup sends the page only to your own PMOS.
|
|
46
|
+
|
|
33
47
|
### Point at a part
|
|
34
48
|
|
|
35
49
|
Keep **Point and say** on. Click a part of the preview and write what to change there. A numbered marker shows the spot. Your next turn sends the markers with your words.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
/* extension-cli.js — `pmos extension`: where the Chrome extension
|
|
2
|
+
/* extension-cli.js — `pmos extension`: where the PMOS Chrome extension is (the Meet panel and the button that adds a page to a report design), and how to load it (initiatives c3-meet-extension, report-design-studio).
|
|
3
3
|
*
|
|
4
4
|
* pmos extension print the absolute path of workspace/extension and the steps to load it in Chrome
|
|
5
5
|
*
|
|
@@ -10,20 +10,22 @@ const path = require('path');
|
|
|
10
10
|
|
|
11
11
|
const COMMANDS = { extension: [] };
|
|
12
12
|
const FOLDER = path.resolve(__dirname, '..', 'extension');
|
|
13
|
-
const USAGE = ['usage: pmos extension — the Chrome extension
|
|
13
|
+
const USAGE = ['usage: pmos extension — the Chrome extension: the meeting companion over a Google Meet call, and a button that adds a page to a report design'];
|
|
14
14
|
|
|
15
15
|
/** The lines `pmos extension` prints for a folder. Pure. */
|
|
16
16
|
function lines(folder, exists = true) {
|
|
17
17
|
const out = [];
|
|
18
18
|
if (!exists) { out.push(`✗ the extension folder is not here: ${folder}`, ' Reinstall the workspace with `pmos upgrade`.'); return out; }
|
|
19
|
-
out.push('PMOS for
|
|
19
|
+
out.push('PMOS for Chrome', '', ` folder ${folder}`, '',
|
|
20
20
|
'Load it in three steps:',
|
|
21
21
|
' 1. In Chrome, open chrome://extensions',
|
|
22
22
|
' 2. Turn on Developer mode (top right)',
|
|
23
23
|
' 3. Press Load unpacked, then pick the folder above',
|
|
24
24
|
'',
|
|
25
25
|
'PMOS is looked for at http://localhost:7433. If pmos printed another port, open the extension\'s Options and set that address.',
|
|
26
|
-
'Then open a Google Meet call: the panel shows at the bottom right.'
|
|
26
|
+
'Then open a Google Meet call: the panel shows at the bottom right.',
|
|
27
|
+
'To add a page to a report design, open any page and press the PMOS button in the toolbar. Choose the report, then Add as a sample.',
|
|
28
|
+
'After an update, press the reload arrow on the extension\'s card in chrome://extensions.');
|
|
27
29
|
return out;
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (initiative workspace-headless-jobs; PM decision 2026-10-11: keep Jev, no direct Claude API, background
|
|
4
4
|
* jobs that need no human run headless).
|
|
5
5
|
*
|
|
6
|
-
* kindFor({ agent, origin, skill, env }) → 'notes' | 'skill' | 'ask' | 'drive' | 'scan-web' | 'scan-repo' | 'scan-research-web' | 'scan-research-repo' | 'report-setup' | 'report-find' | 'core-doc' | 'report-design' | null (null: open a pane, as before)
|
|
6
|
+
* kindFor({ agent, origin, skill, env }) → 'notes' | 'skill' | 'ask' | 'drive' | 'scan-web' | 'scan-repo' | 'scan-research-web' | 'scan-research-repo' | 'report-setup' | 'report-find' | 'core-doc' | 'report-design' | 'report-sample-fetch' | null (null: open a pane, as before)
|
|
7
7
|
* argsFor(kind, { modelArgs, addDirs, writePaths }) → the argv after `claude` (writePaths: the one render file a report-design job may write)
|
|
8
8
|
* parseResult(stdout) → { ok, text, isError, turns, durationMs, costUsd, denied[] }
|
|
9
9
|
* streamLines(line) → { log: [lines], result: object|null } one stream-json event, for the live log
|
|
@@ -80,7 +80,9 @@ const REPORT_FIND_ORIGINS = Object.freeze(['report-find:']);
|
|
|
80
80
|
const CORE_DOC_ORIGINS = Object.freeze(['core-doc:']);
|
|
81
81
|
// report-design-studio: a studio turn, a cycle render or a re-render to match (origin `report-design:<cadence>`).
|
|
82
82
|
const REPORT_DESIGN_ORIGINS = Object.freeze(['report-design:']);
|
|
83
|
-
|
|
83
|
+
// report-design-studio: reading ONE linked Google or Confluence file through the PM's own connectors (origin `report-sample-fetch:<cadence>:<sampleId>`).
|
|
84
|
+
const REPORT_SAMPLE_FETCH_ORIGINS = Object.freeze(['report-sample-fetch:']);
|
|
85
|
+
const WRITE_PATH_KINDS = Object.freeze(['report-design', 'report-sample-fetch']); // kinds whose one writable file the caller names (argsFor writePaths)
|
|
84
86
|
|
|
85
87
|
// What each kind may use. Read-only search is always allowed. The notes passes write their JSON (and rename the .tmp
|
|
86
88
|
// file) in their notes folder, which --add-dir opens to them; the skills also run `pmos` verbs and repository scripts.
|
|
@@ -110,6 +112,8 @@ const TOOLS = Object.freeze({
|
|
|
110
112
|
'core-doc': Object.freeze(['Read', 'Glob', 'Grep', 'Bash(pmos coredoc write *)']),
|
|
111
113
|
// The design run: read and search only. Its one write, the render file, is added per run by argsFor (writePaths). No Bash, no web, nothing that sends.
|
|
112
114
|
'report-design': Object.freeze(['Read', 'Glob', 'Grep']),
|
|
115
|
+
// The link fetch run: Read, and its one write (the text copy of the linked file) is added per run by argsFor. No Bash, no web, no connector tool named.
|
|
116
|
+
'report-sample-fetch': Object.freeze(['Read']),
|
|
113
117
|
skill: Object.freeze(['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash(pmos *)', 'Bash(node *)',
|
|
114
118
|
'Bash(git log *)', 'Bash(git status *)', 'Bash(git diff *)', 'Bash(ls *)', 'Bash(date *)']),
|
|
115
119
|
});
|
|
@@ -134,6 +138,7 @@ function kindFor({ agent, origin, skill, env } = {}) {
|
|
|
134
138
|
if (REPORT_FIND_ORIGINS.some((p) => o.startsWith(p))) return 'report-find';
|
|
135
139
|
if (CORE_DOC_ORIGINS.some((p) => o.startsWith(p))) return 'core-doc';
|
|
136
140
|
if (REPORT_DESIGN_ORIGINS.some((p) => o.startsWith(p))) return 'report-design';
|
|
141
|
+
if (REPORT_SAMPLE_FETCH_ORIGINS.some((p) => o.startsWith(p))) return 'report-sample-fetch';
|
|
137
142
|
if (skill && HEADLESS_SKILLS.includes(String(skill))) return 'skill';
|
|
138
143
|
return null;
|
|
139
144
|
}
|
|
@@ -141,7 +146,7 @@ function kindFor({ agent, origin, skill, env } = {}) {
|
|
|
141
146
|
function argsFor(kind, { modelArgs = [], addDirs = [], writePaths = [] } = {}) {
|
|
142
147
|
if (!TOOLS[kind]) throw new Error('unknown headless kind: ' + kind);
|
|
143
148
|
const tools = TOOLS[kind].slice();
|
|
144
|
-
// A report-design run may write its render file and nothing else: one absolute path, no wildcard, no comma.
|
|
149
|
+
// A report-design run may write its render file (a report-sample-fetch run, its text copy) and nothing else: one absolute path, no wildcard, no comma.
|
|
145
150
|
if (WRITE_PATH_KINDS.includes(kind)) {
|
|
146
151
|
for (const p of Array.isArray(writePaths) ? writePaths : []) {
|
|
147
152
|
const s = String(p || '');
|
|
@@ -260,4 +265,4 @@ function logLines(r, exitCode) {
|
|
|
260
265
|
return out;
|
|
261
266
|
}
|
|
262
267
|
|
|
263
|
-
module.exports = { HEADLESS_SKILLS, HEADLESS_ORIGINS, ASK_ORIGINS, DRIVE_ORIGINS, SCAN_WEB_ORIGINS, SCAN_REPO_ORIGINS, SCAN_RESEARCH_WEB_ORIGINS, SCAN_RESEARCH_REPO_ORIGINS, WEB_KINDS, REPORT_SETUP_ORIGINS, REPORT_FIND_ORIGINS, CORE_DOC_ORIGINS, REPORT_DESIGN_ORIGINS, TOOLS, DEFAULT_TIMEOUT_MS, kindFor, argsFor, timeoutMs, parseResult, streamLines, outcomeOf, logLines, switchedOff };
|
|
268
|
+
module.exports = { HEADLESS_SKILLS, HEADLESS_ORIGINS, ASK_ORIGINS, DRIVE_ORIGINS, SCAN_WEB_ORIGINS, SCAN_REPO_ORIGINS, SCAN_RESEARCH_WEB_ORIGINS, SCAN_RESEARCH_REPO_ORIGINS, WEB_KINDS, REPORT_SETUP_ORIGINS, REPORT_FIND_ORIGINS, CORE_DOC_ORIGINS, REPORT_DESIGN_ORIGINS, REPORT_SAMPLE_FETCH_ORIGINS, TOOLS, DEFAULT_TIMEOUT_MS, kindFor, argsFor, timeoutMs, parseResult, streamLines, outcomeOf, logLines, switchedOff };
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* THE LINES of design.jsonl. Every line is { ev, …, at, actor }. `actor` is the guard's actor for a press, or { by: 'pmos', via: 'job' } for a job:
|
|
20
20
|
* describe { text, format } the PM's plain words, and Deck or Document
|
|
21
21
|
* sample { id, name, kind, source, ext, readable } a sample was added (the file is samples/<id>.<ext>, its record samples/<id>.json)
|
|
22
|
+
* fetched { id, ok, via?, title?, reason? } a link sample was read through the PM's connectors (ok), or could not be (reason); the words are samples/<id>.txt
|
|
22
23
|
* took { id, took, readable } what a job took from a sample (or that it could not read it)
|
|
23
24
|
* turn { text, points, base, job } a refine turn started a job; `base` is the version it builds on
|
|
24
25
|
* version { n, format, turn, answer, from } a job (or an adopted cycle) landed D<n>
|
|
@@ -44,9 +45,10 @@ const zlib = require('zlib');
|
|
|
44
45
|
const RD = require('./report-draft.js');
|
|
45
46
|
const issues = require('./issues.js');
|
|
46
47
|
|
|
47
|
-
const EVENTS = Object.freeze(['describe', 'sample', 'took', 'turn', 'version', 'accept', 'revert', 'error', 'cycle-decision']);
|
|
48
|
+
const EVENTS = Object.freeze(['describe', 'sample', 'fetched', 'took', 'turn', 'version', 'accept', 'revert', 'error', 'cycle-decision']);
|
|
48
49
|
const FORMATS = Object.freeze(['deck', 'doc']);
|
|
49
|
-
const SAMPLE_KINDS = Object.freeze(['pdf', 'pptx', 'docx', 'image', 'link', 'pmos-doc']);
|
|
50
|
+
const SAMPLE_KINDS = Object.freeze(['pdf', 'pptx', 'docx', 'image', 'pictures', 'link', 'pmos-doc']);
|
|
51
|
+
const MAX_PICTURES = 12;
|
|
50
52
|
const MAX_SAMPLE_BYTES = 20 * 1024 * 1024;
|
|
51
53
|
const MAX_THUMB_BYTES = 256 * 1024;
|
|
52
54
|
const MAX_TEXT = 4000;
|
|
@@ -117,8 +119,12 @@ function fold(events) {
|
|
|
117
119
|
if (FORMATS.includes(l.format)) st.format = l.format;
|
|
118
120
|
st.log.push({ at: l.at, who: 'you', kind: 'describe', text: str(l.text) });
|
|
119
121
|
} else if (l.ev === 'sample') {
|
|
120
|
-
st.samples.set(l.id, { id: l.id, name: str(l.name), kind: l.kind, source: str(l.source), ext: str(l.ext), took: null, readable: l.readable !== false, at: l.at
|
|
122
|
+
st.samples.set(l.id, { id: l.id, name: str(l.name), kind: l.kind, source: str(l.source), ext: str(l.ext), took: null, readable: l.readable !== false, at: l.at,
|
|
123
|
+
...(l.via === 'chrome' ? { via: 'chrome' } : {}), ...(Number.isInteger(l.count) ? { count: l.count } : {}), ...(Number.isInteger(l.pages) ? { pages: l.pages } : {}) });
|
|
121
124
|
st.log.push({ at: l.at, who: 'you', kind: 'sample', text: str(l.name), sample: l.id });
|
|
125
|
+
} else if (l.ev === 'fetched') {
|
|
126
|
+
const s = st.samples.get(l.id);
|
|
127
|
+
if (s) { s.readable = l.ok === true; s.fetched = l.ok === true; s.fetch_reason = l.ok === true ? null : str(l.reason) || 'no reason'; }
|
|
122
128
|
} else if (l.ev === 'took') {
|
|
123
129
|
const s = st.samples.get(l.id);
|
|
124
130
|
if (s) { if (typeof l.took === 'string' && l.took) s.took = l.took; if (typeof l.readable === 'boolean') s.readable = l.readable; }
|
|
@@ -204,7 +210,7 @@ function addSample(root, cadence, input, { actor, now } = {}) {
|
|
|
204
210
|
let u; try { u = new URL(link); } catch { return refuse(400, 'link must be a full address, such as https://…'); }
|
|
205
211
|
if (u.protocol !== 'https:' && u.protocol !== 'http:') return refuse(400, 'link must be an http or https address');
|
|
206
212
|
if (link.length > 2000) return refuse(400, 'link is too long');
|
|
207
|
-
// the job reads files only (Read, Glob, Grep), so a link is kept by its address
|
|
213
|
+
// the job reads files only (Read, Glob, Grep), so a link is kept by its address; a Google or Confluence link is then read by the fetch run (recordFetched)
|
|
208
214
|
rec = { id, name: oneLine(input.name || linkName(u), 120), kind: 'link', source: link, ext: '', readable: false, thumb: null, text_file: null };
|
|
209
215
|
} else if (typeof input.doc === 'string' && input.doc.trim()) {
|
|
210
216
|
const rel = input.doc.trim().replace(/\\/g, '/');
|
|
@@ -216,6 +222,32 @@ function addSample(root, cadence, input, { actor, now } = {}) {
|
|
|
216
222
|
writeAtomic(path.join(p.samples, `${id}.md`), text);
|
|
217
223
|
const fm = /^---\r?\n[\s\S]*?\btitle:\s*["']?([^\n"']+)/.exec(text);
|
|
218
224
|
rec = { id, name: oneLine(input.name || (fm ? fm[1] : rel.split('/').slice(-2).join(' ')), 120), kind: 'pmos-doc', source: rel, ext: 'md', readable: true, thumb: null, text_file: null };
|
|
225
|
+
} else if (str(input.kind) === 'pictures' || Array.isArray(input.pictures)) {
|
|
226
|
+
// pictures of a whole page, scrolled top to bottom, from the Chrome extension (the page frames PMOS; see extension/popup.js)
|
|
227
|
+
const list = Array.isArray(input.pictures) ? input.pictures : null;
|
|
228
|
+
if (str(input.kind) !== 'pictures' || !list || !list.length) return refuse(400, 'pictures needs kind "pictures" and a list of pictures (base64 JPEG or PNG)');
|
|
229
|
+
if (list.length > MAX_PICTURES) return refuse(400, `at most ${MAX_PICTURES} pictures in one sample`);
|
|
230
|
+
const bufs = [];
|
|
231
|
+
let total = 0;
|
|
232
|
+
for (const [i, raw] of list.entries()) {
|
|
233
|
+
if (typeof raw !== 'string' || !raw) return refuse(400, `picture ${i + 1} is empty`);
|
|
234
|
+
const b64 = raw.replace(/^data:[^,]*,/, '');
|
|
235
|
+
total += Math.floor(b64.length * 3 / 4);
|
|
236
|
+
if (total > MAX_SAMPLE_BYTES + 4 * list.length) return refuse(413, 'a sample is at most 20 MB');
|
|
237
|
+
const buf = Buffer.from(b64, 'base64');
|
|
238
|
+
const s = buf.length ? sniff(buf) : null;
|
|
239
|
+
if (!s || s.kind !== 'image') return refuse(415, `picture ${i + 1} must be a PNG or a JPG`);
|
|
240
|
+
bufs.push({ buf, ext: s.ext });
|
|
241
|
+
}
|
|
242
|
+
if (bufs.reduce((n, b) => n + b.buf.length, 0) > MAX_SAMPLE_BYTES) return refuse(413, 'a sample is at most 20 MB');
|
|
243
|
+
fs.mkdirSync(p.samples, { recursive: true });
|
|
244
|
+
const files = bufs.map((b, i) => { const f = `${id}-${i + 1}.${b.ext}`; writeAtomic(path.join(p.samples, f), b.buf); return f; });
|
|
245
|
+
const first = bufs[0];
|
|
246
|
+
const thumb = first.buf.length <= MAX_THUMB_BYTES ? `data:image/${first.ext === 'jpg' ? 'jpeg' : 'png'};base64,${first.buf.toString('base64')}` : null;
|
|
247
|
+
const src = typeof input.source_url === 'string' ? input.source_url.trim() : '';
|
|
248
|
+
const u = parseUrl(src);
|
|
249
|
+
const source = u && /^https?:$/.test(u.protocol) && src.length <= 2000 ? src : 'upload';
|
|
250
|
+
rec = { id, name: oneLine(input.name || 'Page pictures', 120), kind: 'pictures', source, ext: first.ext, count: files.length, files, via: 'chrome', bytes: bufs.reduce((n, b) => n + b.buf.length, 0), readable: true, thumb, text_file: null };
|
|
219
251
|
} else {
|
|
220
252
|
const kind = str(input.kind);
|
|
221
253
|
if (!['pdf', 'pptx', 'docx', 'image'].includes(kind)) return refuse(400, 'kind must be pdf, pptx, docx or image (or send a link, or a report doc)');
|
|
@@ -240,27 +272,78 @@ function addSample(root, cadence, input, { actor, now } = {}) {
|
|
|
240
272
|
try { thumb = ooxmlThumb(buf); } catch { thumb = null; }
|
|
241
273
|
}
|
|
242
274
|
rec = { id, name: oneLine(input.name || `${kind} sample`, 120), kind, source: 'upload', ext, bytes: buf.length, readable, thumb, text_file: textFile };
|
|
275
|
+
if (kind === 'pdf' && input.from === 'chrome') {
|
|
276
|
+
// a PDF the Chrome extension fetched: where it came from and how many pages it has
|
|
277
|
+
const src = typeof input.source_url === 'string' ? input.source_url.trim() : '';
|
|
278
|
+
const u = parseUrl(src);
|
|
279
|
+
if (u && /^https?:$/.test(u.protocol) && src.length <= 2000) rec.source = src;
|
|
280
|
+
rec.via = 'chrome';
|
|
281
|
+
const n = Number(input.pages);
|
|
282
|
+
if (Number.isInteger(n) && n > 0 && n < 5000) rec.pages = n;
|
|
283
|
+
}
|
|
243
284
|
}
|
|
244
285
|
writeAtomic(path.join(p.samples, `${id}.json`), JSON.stringify({ ...rec, took: null, added_at: iso(now) }, null, 2) + '\n');
|
|
245
|
-
append(root, cadence, { ev: 'sample', id, name: rec.name, kind: rec.kind, source: rec.source, ext: rec.ext, readable: rec.readable
|
|
286
|
+
append(root, cadence, { ev: 'sample', id, name: rec.name, kind: rec.kind, source: rec.source, ext: rec.ext, readable: rec.readable,
|
|
287
|
+
...(rec.via ? { via: rec.via } : {}), ...(rec.count ? { count: rec.count } : {}), ...(rec.pages ? { pages: rec.pages } : {}) }, { actor, now });
|
|
246
288
|
return { ok: true, sample: sampleOut(rec) };
|
|
247
289
|
}
|
|
248
|
-
function sampleOut(s) {
|
|
290
|
+
function sampleOut(s, { fetching } = {}) {
|
|
249
291
|
const out = { id: s.id, name: s.name, kind: s.kind, source: s.source, took: s.took || null, thumb: typeof s.thumb === 'string' && s.thumb.startsWith('data:image/') ? s.thumb : null, readable: s.readable !== false };
|
|
250
|
-
if (s.
|
|
292
|
+
if (s.via === 'chrome') {
|
|
293
|
+
out.via = 'chrome';
|
|
294
|
+
out.line = chromeLine(s);
|
|
295
|
+
if (s.count) out.count = s.count;
|
|
296
|
+
if (s.pages) out.pages = s.pages;
|
|
297
|
+
}
|
|
298
|
+
if (s.kind === 'link') {
|
|
299
|
+
if (fetching) { out.fetching = true; out.hint = 'PMOS is reading this link with your connectors.'; }
|
|
300
|
+
else out.hint = linkHint(s.source, { fetched: !!(s.text_file && s.fetched), reason: s.fetch_reason });
|
|
301
|
+
}
|
|
251
302
|
return out;
|
|
252
303
|
}
|
|
304
|
+
/** The kind line of a sample the Chrome extension added: 'From Chrome, PDF, 12 slides' | 'From Chrome, PDF, 3 pages' | 'From Chrome, 3 page pictures'. */
|
|
305
|
+
function chromeLine(s) {
|
|
306
|
+
if (s.kind === 'pictures') { const n = s.count || 0; return n ? `From Chrome, ${n} page ${n === 1 ? 'picture' : 'pictures'}` : 'From Chrome, page pictures'; }
|
|
307
|
+
const g = googleOf(parseUrl(s.source));
|
|
308
|
+
const n = s.pages || 0;
|
|
309
|
+
return n ? `From Chrome, PDF, ${n} ${g && g[2] === 'Slides' ? (n === 1 ? 'slide' : 'slides') : (n === 1 ? 'page' : 'pages')}` : 'From Chrome, PDF';
|
|
310
|
+
}
|
|
253
311
|
/* Google files: name them for what they are, and say which download PMOS can read */
|
|
254
312
|
// a PDF, because the job sees its pages; from a .pptx or .docx it gets the words only, not the look
|
|
255
313
|
const GOOGLE = [[/^\/presentation\//, 'Google Slides deck', 'Slides'], [/^\/document\//, 'Google Docs document', 'Docs'], [/^\/spreadsheets\//, 'Google Sheets sheet', 'Sheets']];
|
|
256
314
|
const googleOf = (u) => (u && /(^|\.)docs\.google\.com$/i.test(u.hostname) ? GOOGLE.find((g) => g[0].test(u.pathname)) || null : null);
|
|
257
315
|
const parseUrl = (v) => { try { return new URL(String(v || '')); } catch { return null; } };
|
|
258
316
|
function linkName(u) { const g = googleOf(u); return g ? g[1] : u.hostname + u.pathname; }
|
|
259
|
-
/** What the PM sees under a link sample: PMOS
|
|
260
|
-
function linkHint(source) {
|
|
317
|
+
/** What the PM sees under a link sample: what PMOS read through the connectors, or why it did not, and how to add a copy that shows the look. */
|
|
318
|
+
function linkHint(source, { fetched = false, reason = null } = {}) {
|
|
261
319
|
const g = googleOf(parseUrl(source));
|
|
262
|
-
|
|
263
|
-
return
|
|
320
|
+
const pdfStep = g ? ` In ${g[2]}, choose File, Download, PDF document (.pdf), then add that file here.` : ' Save the page as a PDF or take a screenshot, then add that file here.';
|
|
321
|
+
if (fetched) return `PMOS read the words and structure of this link. To match its look, also add it as a PDF or a screenshot.${g ? pdfStep : ''}`;
|
|
322
|
+
if (g) return `${reason ? 'Your connectors could not read it. ' : ''}PMOS cannot open ${g[2]} from a link, because it needs your Google sign-in. In ${g[2]}, choose File, Download, PDF document (.pdf), then add that file here. PMOS sees the pages of a PDF, so it can follow their look.`;
|
|
323
|
+
return `${reason ? 'Your connectors could not read it. ' : ''}PMOS cannot open web pages. Save the page as a PDF or take a screenshot, then add that file here.`;
|
|
324
|
+
}
|
|
325
|
+
/** 'Google Slides deck' | 'Google Docs document' | 'Google Sheets sheet' for a Google link, else null. */
|
|
326
|
+
function googleKind(source) { const g = googleOf(parseUrl(source)); return g ? g[1] : null; }
|
|
327
|
+
/** True for a link the fetch run can try: a Google Slides, Docs or Sheets file, or a Confluence page. */
|
|
328
|
+
function fetchableLink(source) {
|
|
329
|
+
const u = parseUrl(source);
|
|
330
|
+
if (!u || !/^https?:$/.test(u.protocol)) return false;
|
|
331
|
+
if (googleOf(u)) return true;
|
|
332
|
+
return /(^|\.)atlassian\.net$/i.test(u.hostname) && /^\/wiki\//.test(u.pathname);
|
|
333
|
+
}
|
|
334
|
+
/** A fetch run read the link: samples/<id>.txt holds its words (ok), or the link could not be read (reason). Appends a 'fetched' event. */
|
|
335
|
+
function recordFetched(root, cadence, id, { ok = true, title, via, reason, now } = {}) {
|
|
336
|
+
if (!SAMPLE_ID_RE.test(String(id))) return false;
|
|
337
|
+
const p = paths(root, cadence);
|
|
338
|
+
const rec = sampleRecord(root, cadence, id);
|
|
339
|
+
if (!rec || rec.kind !== 'link') return false;
|
|
340
|
+
const good = ok === true;
|
|
341
|
+
const next = { ...rec, readable: good, text_file: good ? `${id}.txt` : null, fetch_reason: good ? null : oneLine(reason || 'no reason', 300),
|
|
342
|
+
...(good ? { fetched_at: iso(now), via: oneLine(via || '', 120) || null } : {}) };
|
|
343
|
+
if (good && typeof title === 'string' && title.trim()) next.title = oneLine(title, 160);
|
|
344
|
+
writeAtomic(path.join(p.samples, `${id}.json`), JSON.stringify(next, null, 2) + '\n');
|
|
345
|
+
append(root, cadence, { ev: 'fetched', id, ok: good, ...(good ? { via: next.via, title: next.title || null } : { reason: next.fetch_reason }) }, { now });
|
|
346
|
+
return true;
|
|
264
347
|
}
|
|
265
348
|
/** Record what a job took from a sample (also kept in samples/<id>.json). */
|
|
266
349
|
function recordTook(root, cadence, id, took, readable, { now } = {}) {
|
|
@@ -274,21 +357,28 @@ function recordTook(root, cadence, id, took, readable, { now } = {}) {
|
|
|
274
357
|
append(root, cadence, { ev: 'took', id, took: t, ...(rd === undefined ? {} : { readable: rd }) }, { now });
|
|
275
358
|
return true;
|
|
276
359
|
}
|
|
277
|
-
/** The file the job reads for a sample: the pdf or image itself, the extracted words of a .pptx/.docx, the copy of a report doc; null for
|
|
360
|
+
/** The file the job reads for a sample: the pdf or image itself, the extracted words of a .pptx/.docx, the copy of a report doc, the text copy of a fetched link; null for any other link. */
|
|
278
361
|
function sampleReadPath(root, cadence, s) {
|
|
279
362
|
const p = paths(root, cadence);
|
|
280
|
-
if (s.kind === '
|
|
363
|
+
if (s.kind === 'pictures') {
|
|
364
|
+
// every picture of the page, in order (samples/<id>-1.jpg, <id>-2.jpg, …); null when none is there
|
|
365
|
+
let names = [];
|
|
366
|
+
try { names = fs.readdirSync(p.samples).filter((f) => f.startsWith(`${s.id}-`) && /^\d+\.(jpg|png)$/.test(f.slice(s.id.length + 1))); } catch { names = []; }
|
|
367
|
+
names.sort((a, b) => parseInt(a.slice(s.id.length + 1), 10) - parseInt(b.slice(s.id.length + 1), 10));
|
|
368
|
+
return names.length ? names.map((f) => path.join(p.samples, f)) : null;
|
|
369
|
+
}
|
|
370
|
+
if (s.kind === 'link') { const f = path.join(p.samples, `${s.id}.txt`); return s.fetched && fs.existsSync(f) ? f : null; }
|
|
281
371
|
if (s.kind === 'pptx' || s.kind === 'docx') { const f = path.join(p.samples, `${s.id}.txt`); return fs.existsSync(f) ? f : null; }
|
|
282
372
|
const f = path.join(p.samples, `${s.id}.${s.ext}`);
|
|
283
373
|
return fs.existsSync(f) ? f : null;
|
|
284
374
|
}
|
|
285
375
|
|
|
286
376
|
// ── the GET answer ────────────────────────────────────────────────────────────────────────────────────────
|
|
287
|
-
function designAnswer(root, cadence, { running } = {}) {
|
|
377
|
+
function designAnswer(root, cadence, { running, fetching } = {}) {
|
|
288
378
|
const st = stateOf(root, cadence);
|
|
289
379
|
const samples = [...st.samples.values()].map((s) => {
|
|
290
380
|
const rec = sampleRecord(root, cadence, s.id) || {};
|
|
291
|
-
return sampleOut({ ...s, thumb: rec.thumb || null, took: s.took || rec.took || null });
|
|
381
|
+
return sampleOut({ ...s, thumb: rec.thumb || null, took: s.took || rec.took || null, text_file: rec.text_file || null }, { fetching: !!(fetching && fetching.has(s.id)) });
|
|
292
382
|
});
|
|
293
383
|
const versions = [...st.versions.values()].sort((a, b) => a.n - b.n).map((v) => {
|
|
294
384
|
const m = versionMeta(root, cadence, v.n) || {};
|
|
@@ -320,7 +410,7 @@ function writeBrief(root, cadence, setup, { now } = {}) {
|
|
|
320
410
|
'## Description', '', st.description ? st.description : 'No description was given.', '', '## Samples', ''];
|
|
321
411
|
const samples = [...st.samples.values()];
|
|
322
412
|
if (!samples.length) lines.push('No samples.');
|
|
323
|
-
for (const s of samples) lines.push(`- ${s.name} (${s.kind}${s.kind === 'link' || s.kind === 'pmos-doc' ? ', ' + s.source : ''}): ${s.took ? 'PMOS took ' + s.took : (s.readable ? 'nothing taken yet' : 'could not be read')}`);
|
|
413
|
+
for (const s of samples) lines.push(`- ${s.name} (${s.kind}${s.kind === 'link' || s.kind === 'pmos-doc' ? ', ' + s.source : ''}${s.via === 'chrome' ? ', from Chrome' : ''}): ${s.took ? 'PMOS took ' + s.took : (s.readable ? 'nothing taken yet' : 'could not be read')}`);
|
|
324
414
|
lines.push('', '## Reference outline', '');
|
|
325
415
|
for (const o of (Array.isArray(meta.outline) ? meta.outline : [])) lines.push(`- ${o.id} (${o.kind || 'slide'}): ${o.title || ''}`);
|
|
326
416
|
const sec = (h, list) => { lines.push('', `## ${h}`, ''); if (!list || !list.length) lines.push('None written yet.'); else for (const x of list) lines.push(`- ${x}`); };
|
|
@@ -749,7 +839,7 @@ function currentCycle(root, cadence) {
|
|
|
749
839
|
module.exports = {
|
|
750
840
|
EVENTS, FORMATS, SAMPLE_KINDS, MAX_SAMPLE_BYTES, CHOICES, PIN_OPS, CADENCE_RE, SLOT_RE, SLIDE_ID_RE, PMOS_ACTOR,
|
|
751
841
|
paths, append, readEvents, fold, stateOf, designAnswer, versionMeta, versionHtml, writeVersion, rulesFor, cleanRules,
|
|
752
|
-
addSample, sampleOut, sampleRecord, recordTook, sampleReadPath, writeBrief,
|
|
842
|
+
addSample, sampleOut, sampleRecord, recordTook, recordFetched, fetchableLink, googleKind, sampleReadPath, writeBrief,
|
|
753
843
|
tokenize, sanitize, cleanCss, outlineOf, formatOf, diffOutline,
|
|
754
844
|
cyclePaths, readCycle, writeCycle, decideCycle, isIssuedSlot, docSha, currentCycle, previousCycle,
|
|
755
845
|
notesOf, setSpeaker, pinOp, ooxmlText, ooxmlThumb, unzipSome,
|