pinokiod 7.2.16 → 7.2.18

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.
Files changed (51) hide show
  1. package/kernel/agent_instructions.js +166 -0
  2. package/kernel/api/index.js +137 -12
  3. package/kernel/bin/huggingface.js +1 -1
  4. package/kernel/environment.js +23 -9
  5. package/kernel/plugin_sources.js +57 -4
  6. package/kernel/prototype.js +4 -0
  7. package/kernel/shell.js +2 -0
  8. package/kernel/watch/index.js +31 -4
  9. package/package.json +1 -1
  10. package/server/features/index.js +4 -4
  11. package/server/features/{drafts → notes}/index.js +9 -9
  12. package/server/features/{drafts → notes}/parser.js +12 -7
  13. package/server/features/notes/public/notes.css +955 -0
  14. package/server/features/notes/public/notes.js +1149 -0
  15. package/server/features/{drafts → notes}/registry_import.js +59 -74
  16. package/server/features/notes/routes.js +156 -0
  17. package/server/features/notes/service.js +326 -0
  18. package/server/features/{drafts → notes}/watcher.js +14 -16
  19. package/server/index.js +61 -30
  20. package/server/lib/content_validation.js +19 -8
  21. package/server/lib/workspace_catalog.js +18 -18
  22. package/server/public/task-launcher.css +11 -3
  23. package/server/public/tasker.css +336 -0
  24. package/server/public/tasker.js +407 -0
  25. package/server/views/d.ejs +33 -2
  26. package/server/views/partials/menu.ejs +1 -1
  27. package/server/views/partials/workspace_row.ejs +11 -11
  28. package/server/views/pre.ejs +1 -1
  29. package/server/views/task_launch.ejs +10 -10
  30. package/server/views/tasker.ejs +40 -0
  31. package/server/views/terminal.ejs +15 -6
  32. package/server/views/terminals.ejs +0 -1
  33. package/server/views/workspaces.ejs +2 -1
  34. package/system/plugin/antigravity/pinokio.js +2 -4
  35. package/system/plugin/claude/pinokio.js +2 -4
  36. package/system/plugin/claude-auto/pinokio.js +2 -4
  37. package/system/plugin/claude-desktop/pinokio.js +2 -4
  38. package/system/plugin/codex/pinokio.js +2 -4
  39. package/system/plugin/codex-auto/pinokio.js +2 -4
  40. package/system/plugin/codex-desktop/pinokio.js +2 -4
  41. package/system/plugin/crush/pinokio.js +2 -4
  42. package/system/plugin/cursor/pinokio.js +2 -4
  43. package/system/plugin/gemini/pinokio.js +2 -4
  44. package/system/plugin/gemini-auto/pinokio.js +2 -4
  45. package/system/plugin/qwen/pinokio.js +2 -4
  46. package/system/plugin/vscode/pinokio.js +2 -4
  47. package/system/plugin/windsurf/pinokio.js +2 -4
  48. package/test/plugin-sources.test.js +45 -0
  49. package/server/features/drafts/public/drafts.js +0 -1569
  50. package/server/features/drafts/routes.js +0 -68
  51. package/server/features/drafts/service.js +0 -261
@@ -45,7 +45,7 @@ function renderMessage(res, status, title, message) {
45
45
  </html>`)
46
46
  }
47
47
 
48
- function renderImportLauncher(res, { authorizeUrl, draftId, registryOrigin, autoOpen }) {
48
+ function renderImportLauncher(res, { authorizeUrl, autoOpen }) {
49
49
  res.status(200).send(`<!doctype html>
50
50
  <html>
51
51
  <head>
@@ -63,68 +63,48 @@ function renderImportLauncher(res, { authorizeUrl, draftId, registryOrigin, auto
63
63
  <body>
64
64
  <div class="box">
65
65
  <h1>Import draft</h1>
66
- <p id="status">${autoOpen ? "Opening the registry authorization window..." : "Click Open registry to authorize the import."}</p>
66
+ <p id="status">${autoOpen ? "Opening the registry authorization page in your browser..." : "Click Open registry to authorize the import."}</p>
67
67
  <button id="open" type="button">Open registry</button>
68
- <div class="muted" style="margin-top:12px;">Keep this Pinokio window open until the registry draft editor opens.</div>
68
+ <div class="muted" style="margin-top:12px;">The registry will return to Pinokio after authorization.</div>
69
69
  </div>
70
70
  <script>
71
71
  window.__PINOKIO_DRAFT_IMPORT_VERSION = "metadata-b64";
72
72
  const authorizeUrl = ${JSON.stringify(authorizeUrl)};
73
- const draftId = ${JSON.stringify(draftId)};
74
- const registryOrigin = ${JSON.stringify(registryOrigin)};
75
73
  const autoOpen = ${JSON.stringify(Boolean(autoOpen))};
76
74
  const statusEl = document.getElementById("status");
77
75
  const openButton = document.getElementById("open");
78
- let registryWindow = null;
79
76
 
80
77
  function setStatus(message) {
81
78
  statusEl.textContent = message;
82
79
  }
83
80
 
84
- function openRegistry() {
85
- registryWindow = window.open(authorizeUrl, "_blank");
86
- if (!registryWindow) {
87
- setStatus("The registry window was blocked. Click Open registry to continue.");
88
- } else {
89
- setStatus("Authorize the import in the registry window.");
90
- }
91
- }
92
-
93
- async function completeImport(payload) {
94
- setStatus("Uploading draft to the registry...");
95
- const response = await fetch("/registry/draft-import/complete", {
81
+ async function openRegistry() {
82
+ setStatus("Opening registry in your browser...");
83
+ const response = await fetch("/pinokio/open", {
96
84
  method: "POST",
97
85
  headers: { "Content-Type": "application/json" },
98
86
  body: JSON.stringify({
99
- draft: draftId,
100
- token: payload.token,
101
- registry: payload.registry,
102
- app: payload.app || ""
87
+ url: authorizeUrl,
88
+ surface: "browser"
103
89
  })
104
90
  });
105
- const data = await response.json().catch(() => null);
106
- if (!response.ok || !data || !data.editUrl) {
107
- const detail = data && data.status ? " (" + data.status + ")" : "";
108
- throw new Error(((data && data.error) || "Import failed.") + detail);
91
+ if (!response.ok) {
92
+ throw new Error("Unable to open registry.");
109
93
  }
110
- try {
111
- if (registryWindow && !registryWindow.closed) registryWindow.close();
112
- } catch (_) {}
113
- window.location.href = data.editUrl;
94
+ setStatus("Authorize the import in your browser.");
114
95
  }
115
96
 
116
- window.addEventListener("message", (event) => {
117
- if (event.origin !== registryOrigin) return;
118
- const payload = event.data || {};
119
- if (payload.type !== "pinokio:draft-import-token" || !payload.token || !payload.registry) return;
120
- completeImport(payload).catch((error) => {
121
- setStatus(error && error.message ? error.message : "Import failed.");
97
+ openButton.addEventListener("click", () => {
98
+ openRegistry().catch((error) => {
99
+ setStatus(error && error.message ? error.message : "Unable to open registry.");
122
100
  });
123
101
  });
124
-
125
- openButton.addEventListener("click", openRegistry);
126
102
  if (autoOpen) {
127
- window.setTimeout(openRegistry, 100);
103
+ window.setTimeout(() => {
104
+ openRegistry().catch((error) => {
105
+ setStatus(error && error.message ? error.message : "Unable to open registry.");
106
+ });
107
+ }, 100);
128
108
  }
129
109
  </script>
130
110
  </body>
@@ -150,10 +130,25 @@ function requestOrigin(req) {
150
130
  return `${req.protocol || "http"}://${host}`
151
131
  }
152
132
 
153
- async function findDraftById(drafts, id) {
133
+ function buildDraftImportReturnUrl(req, item, bundle) {
134
+ const returnUrl = new URL("/registry/draft-import/callback", requestOrigin(req))
135
+ returnUrl.searchParams.set("draft", item.id)
136
+ if (bundle.appSlug) returnUrl.searchParams.set("app", bundle.appSlug)
137
+ return returnUrl.toString()
138
+ }
139
+
140
+ function buildAuthorizeUrl(req, registryBase, item, bundle) {
141
+ const authorizeUrl = new URL("/draft-import/authorize", registryBase)
142
+ authorizeUrl.searchParams.set("handoff", "callback")
143
+ authorizeUrl.searchParams.set("return", buildDraftImportReturnUrl(req, item, bundle))
144
+ if (bundle.appSlug) authorizeUrl.searchParams.set("app", bundle.appSlug)
145
+ return authorizeUrl
146
+ }
147
+
148
+ async function findNoteById(notes, id) {
154
149
  const normalized = String(id || "").trim()
155
150
  if (!normalized) return null
156
- const items = await drafts.listPending({})
151
+ const items = await notes.listPending({})
157
152
  return (items || []).find((item) => item && item.id === normalized) || null
158
153
  }
159
154
 
@@ -173,9 +168,9 @@ function normalizeParent(parent) {
173
168
  }
174
169
 
175
170
  async function buildDraftBundle(item, query = {}) {
176
- const markdown = await fs.promises.readFile(item.postPath, "utf8")
177
- const resultDir = path.dirname(item.postPath)
178
- const titleFallback = item.title || (item.workspaceName ? `Draft for ${item.workspaceName}` : "Draft")
171
+ const markdown = await fs.promises.readFile(item.notePath, "utf8")
172
+ const resultDir = path.dirname(item.notePath)
173
+ const titleFallback = item.title || (item.workspaceName ? `Note for ${item.workspaceName}` : "Note")
179
174
  const extracted = extractTitleAndBody(markdown, titleFallback)
180
175
  const metadataTitle = item.metadata && typeof item.metadata.title === "string"
181
176
  ? normalizeTitle(item.metadata.title)
@@ -200,17 +195,17 @@ function preflightBundle(bundle, options = {}) {
200
195
  const maxFiles = Number(options.maxFiles || DEFAULT_MAX_FILES)
201
196
  const maxFileBytes = Number(options.maxFileBytes || DEFAULT_MAX_FILE_BYTES)
202
197
  if (!bundle.title) {
203
- return "Draft title is missing."
198
+ return "Note title is missing."
204
199
  }
205
200
  if (!isRegistryPostPublish(bundle.publish)) {
206
- return "This draft is not configured for registry publishing."
201
+ return "This note is not configured for registry publishing."
207
202
  }
208
203
  if (bundle.media.length > maxFiles) {
209
- return `Draft has ${bundle.media.length} media files. The registry limit is ${maxFiles}.`
204
+ return `Note has ${bundle.media.length} media files. The registry limit is ${maxFiles}.`
210
205
  }
211
206
  const missing = bundle.media.filter((item) => !item.exists)
212
207
  if (missing.length > 0) {
213
- return `Draft references missing media: ${missing.map((item) => item.ref).join(", ")}`
208
+ return `Note references missing media: ${missing.map((item) => item.ref).join(", ")}`
214
209
  }
215
210
  const oversized = bundle.media.find((item) => item.bytes > maxFileBytes)
216
211
  if (oversized) {
@@ -273,10 +268,10 @@ async function uploadBundle(registryBase, token, bundle) {
273
268
  return response.data || {}
274
269
  }
275
270
 
276
- async function uploadDraftFromRequest(drafts, query, token, registryBase, options = {}) {
277
- const item = await findDraftById(drafts, query.draft)
271
+ async function uploadDraftFromRequest(notes, query, token, registryBase, options = {}) {
272
+ const item = await findNoteById(notes, query.draft)
278
273
  if (!item) {
279
- const error = new Error("The local draft is no longer available.")
274
+ const error = new Error("The local note is no longer available.")
280
275
  error.status = 404
281
276
  throw error
282
277
  }
@@ -291,17 +286,17 @@ async function uploadDraftFromRequest(drafts, query, token, registryBase, option
291
286
  }
292
287
 
293
288
  function registerDraftImportRoutes(app, options = {}) {
294
- const drafts = options.drafts
295
- if (!drafts) {
296
- throw new Error("drafts is required")
289
+ const notes = options.notes
290
+ if (!notes) {
291
+ throw new Error("notes is required")
297
292
  }
298
293
  const defaultRegistryUrl = options.defaultRegistryUrl || "https://beta.pinokio.co"
299
294
  const router = express.Router()
300
295
 
301
296
  router.get("/registry/draft-import/authorize-url", asyncHandler(async (req, res) => {
302
- const item = await findDraftById(drafts, req.query.draft)
297
+ const item = await findNoteById(notes, req.query.draft)
303
298
  if (!item) {
304
- return res.status(404).json({ error: "The local draft is no longer available." })
299
+ return res.status(404).json({ error: "The local note is no longer available." })
305
300
  }
306
301
  const bundle = await buildDraftBundle(item, req.query)
307
302
  const problem = preflightBundle(bundle, options)
@@ -312,43 +307,33 @@ function registerDraftImportRoutes(app, options = {}) {
312
307
  if (!registryBase) {
313
308
  return res.status(400).json({ error: "The registry URL is invalid." })
314
309
  }
315
- const authorizeUrl = new URL("/draft-import/authorize", registryBase)
316
- authorizeUrl.searchParams.set("handoff", "post_message")
317
- authorizeUrl.searchParams.set("origin", requestOrigin(req))
318
- authorizeUrl.searchParams.set("wait", "1")
319
- if (bundle.appSlug) authorizeUrl.searchParams.set("app", bundle.appSlug)
310
+ const authorizeUrl = buildAuthorizeUrl(req, registryBase, item, bundle)
320
311
  res.setHeader("Cache-Control", "no-store")
321
312
  return res.json({
322
313
  draftId: item.id,
323
- authorizeUrl: authorizeUrl.toString(),
324
- registryOrigin: new URL(registryBase).origin
314
+ authorizeUrl: authorizeUrl.toString()
325
315
  })
326
316
  }))
327
317
 
328
318
  router.get("/registry/draft-import/start", asyncHandler(async (req, res) => {
329
- const item = await findDraftById(drafts, req.query.draft)
319
+ const item = await findNoteById(notes, req.query.draft)
330
320
  if (!item) {
331
- return renderMessage(res, 404, "Draft not found", "The local draft is no longer available.")
321
+ return renderMessage(res, 404, "Note not found", "The local note is no longer available.")
332
322
  }
333
323
  const bundle = await buildDraftBundle(item, req.query)
334
324
  const problem = preflightBundle(bundle, options)
335
325
  if (problem) {
336
- return renderMessage(res, 400, "Draft is not ready", problem)
326
+ return renderMessage(res, 400, "Note is not ready", problem)
337
327
  }
338
328
  const registryBase = normalizeRegistryBase(req.query.registry, defaultRegistryUrl)
339
329
  if (!registryBase) {
340
330
  return renderMessage(res, 400, "Registry unavailable", "The registry URL is invalid.")
341
331
  }
342
- const authorizeUrl = new URL("/draft-import/authorize", registryBase)
343
- authorizeUrl.searchParams.set("handoff", "post_message")
344
- authorizeUrl.searchParams.set("origin", requestOrigin(req))
345
- if (bundle.appSlug) authorizeUrl.searchParams.set("app", bundle.appSlug)
332
+ const authorizeUrl = buildAuthorizeUrl(req, registryBase, item, bundle)
346
333
  res.setHeader("Cache-Control", "no-store")
347
334
  res.setHeader("Cross-Origin-Opener-Policy", "unsafe-none")
348
335
  return renderImportLauncher(res, {
349
336
  authorizeUrl: authorizeUrl.toString(),
350
- draftId: item.id,
351
- registryOrigin: new URL(registryBase).origin,
352
337
  autoOpen: req.query.auto === "1"
353
338
  })
354
339
  }))
@@ -369,7 +354,7 @@ function registerDraftImportRoutes(app, options = {}) {
369
354
  app: req.body && req.body.app ? String(req.body.app) : ""
370
355
  })
371
356
  const result = await uploadDraftFromRequest(
372
- drafts,
357
+ notes,
373
358
  { draft: req.body && req.body.draft, app: req.body && req.body.app },
374
359
  token,
375
360
  registryBase,
@@ -407,7 +392,7 @@ function registerDraftImportRoutes(app, options = {}) {
407
392
  return renderMessage(res, 400, "Registry unavailable", "The registry URL is invalid.")
408
393
  }
409
394
  try {
410
- const result = await uploadDraftFromRequest(drafts, req.query, token, registryBase, options)
395
+ const result = await uploadDraftFromRequest(notes, req.query, token, registryBase, options)
411
396
  if (result && result.editUrl) {
412
397
  return res.redirect(String(result.editUrl))
413
398
  }
@@ -0,0 +1,156 @@
1
+ const express = require("express")
2
+ const path = require("path")
3
+ const registerDraftImportRoutes = require("./registry_import")
4
+
5
+ function asyncHandler(fn) {
6
+ return (req, res, next) => {
7
+ Promise.resolve(fn(req, res, next)).catch(next)
8
+ }
9
+ }
10
+
11
+ function isInside(candidate, parent) {
12
+ const relative = path.relative(parent, candidate)
13
+ return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
14
+ }
15
+
16
+ function parsePublishConfig(value) {
17
+ if (typeof value !== "string" || !value.trim()) {
18
+ return null
19
+ }
20
+ let parsed
21
+ try {
22
+ parsed = JSON.parse(value)
23
+ } catch (_) {
24
+ return null
25
+ }
26
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
27
+ return null
28
+ }
29
+ const target = String(parsed.target || "").trim().toLowerCase()
30
+ const type = String(parsed.type || "post").trim().toLowerCase()
31
+ if (target !== "registry" || type !== "post") {
32
+ return null
33
+ }
34
+ const publish = { target: "registry", type: "post" }
35
+ const parent = parsed.parent && typeof parsed.parent === "object" && !Array.isArray(parsed.parent)
36
+ ? parsed.parent
37
+ : null
38
+ const parentType = parent ? String(parent.type || "").trim().toLowerCase() : ""
39
+ const parentUrl = parent ? String(parent.url || parent.repoUrl || "").trim() : ""
40
+ if (parentType === "app" && parentUrl) {
41
+ publish.parent = { type: "app", url: parentUrl }
42
+ }
43
+ return publish
44
+ }
45
+
46
+ function registerNoteRoutes(app, options = {}) {
47
+ const notes = options.notes
48
+ if (!notes) {
49
+ throw new Error("notes is required")
50
+ }
51
+ const kernel = options.kernel
52
+
53
+ const router = express.Router()
54
+ router.get("/notes", asyncHandler(async (req, res) => {
55
+ const cwd = typeof req.query.cwd === "string" ? req.query.cwd : ""
56
+ if (cwd && typeof notes.inspectWorkspace === "function") {
57
+ const resolvedCwd = path.resolve(cwd)
58
+ const home = kernel && kernel.homedir ? path.resolve(kernel.homedir) : ""
59
+ if (!home || isInside(resolvedCwd, home)) {
60
+ const publish = parsePublishConfig(req.query.publish)
61
+ const note = publish ? { publish } : undefined
62
+ await notes.inspectWorkspace({ cwd: resolvedCwd, note }).catch(() => null)
63
+ }
64
+ }
65
+ const items = await notes.listPending({ cwd })
66
+ res.json({
67
+ ok: true,
68
+ items
69
+ })
70
+ }))
71
+
72
+ router.get("/notes/:id/media/:index", asyncHandler(async (req, res) => {
73
+ const item = typeof notes.getPendingById === "function"
74
+ ? await notes.getPendingById(req.params.id)
75
+ : null
76
+ if (!item) {
77
+ res.status(404).send("Note not found")
78
+ return
79
+ }
80
+ const index = Number(req.params.index)
81
+ const media = Array.isArray(item.media) && Number.isInteger(index)
82
+ ? item.media[index]
83
+ : null
84
+ if (!media || !media.exists || !media.path) {
85
+ res.status(404).send("Media not found")
86
+ return
87
+ }
88
+ const filePath = path.resolve(media.path)
89
+ const basePath = path.resolve(item.resultDir)
90
+ const relative = path.relative(basePath, filePath)
91
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
92
+ res.status(403).send("Media path is outside the note")
93
+ return
94
+ }
95
+ res.setHeader("Cache-Control", "no-store")
96
+ res.sendFile(filePath)
97
+ }))
98
+
99
+ router.put("/notes/:id", express.text({ type: "*/*", limit: "6mb" }), asyncHandler(async (req, res) => {
100
+ if (typeof notes.savePendingById !== "function") {
101
+ res.status(501).json({ ok: false, error: "Note editing is unavailable." })
102
+ return
103
+ }
104
+ const markdown = typeof req.body === "string"
105
+ ? req.body
106
+ : (req.body && typeof req.body.markdown === "string"
107
+ ? req.body.markdown
108
+ : null)
109
+ if (markdown === null) {
110
+ res.status(400).json({ ok: false, error: "markdown is required" })
111
+ return
112
+ }
113
+ const revision = typeof req.get("x-pinokio-note-revision") === "string"
114
+ ? req.get("x-pinokio-note-revision")
115
+ : (req.body && typeof req.body.revision === "string"
116
+ ? req.body.revision
117
+ : "")
118
+ try {
119
+ const item = await notes.savePendingById(req.params.id, { markdown, revision })
120
+ if (!item) {
121
+ res.status(404).json({ ok: false, error: "Note not found" })
122
+ return
123
+ }
124
+ res.json({ ok: true, item })
125
+ } catch (error) {
126
+ if (error && error.code === "NOTE_CONFLICT") {
127
+ res.status(409).json({
128
+ ok: false,
129
+ error: error.message,
130
+ item: error.item || null
131
+ })
132
+ return
133
+ }
134
+ if (error && (error.code === "NOTE_TOO_LARGE" || error.code === "NOTE_INVALID_PATH")) {
135
+ res.status(400).json({ ok: false, error: error.message })
136
+ return
137
+ }
138
+ throw error
139
+ }
140
+ }))
141
+
142
+ router.get("/notes.js", (req, res) => {
143
+ res.setHeader("Cache-Control", "no-store")
144
+ res.sendFile(path.resolve(__dirname, "public", "notes.js"))
145
+ })
146
+
147
+ router.get("/notes.css", (req, res) => {
148
+ res.setHeader("Cache-Control", "no-store")
149
+ res.sendFile(path.resolve(__dirname, "public", "notes.css"))
150
+ })
151
+
152
+ app.use(router)
153
+ registerDraftImportRoutes(app, options)
154
+ }
155
+
156
+ module.exports = registerNoteRoutes