pinokiod 7.2.18 → 7.3.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.
Files changed (89) hide show
  1. package/Dockerfile +2 -0
  2. package/kernel/api/index.js +13 -179
  3. package/kernel/api/process/index.js +44 -99
  4. package/kernel/bin/conda-python.js +30 -0
  5. package/kernel/bin/conda.js +22 -3
  6. package/kernel/bin/huggingface.js +1 -1
  7. package/kernel/bin/index.js +11 -1
  8. package/kernel/environment.js +11 -205
  9. package/kernel/git.js +13 -0
  10. package/kernel/index.js +1 -64
  11. package/kernel/plugin.js +58 -6
  12. package/kernel/prototype.js +0 -4
  13. package/kernel/shell.js +2 -23
  14. package/kernel/util.js +0 -60
  15. package/package.json +1 -1
  16. package/server/index.js +171 -229
  17. package/server/lib/content_validation.js +33 -47
  18. package/server/public/common.js +29 -103
  19. package/server/public/create-launcher.js +31 -4
  20. package/server/public/electron.css +6 -0
  21. package/server/public/style.css +0 -337
  22. package/server/public/task-launcher.css +3 -11
  23. package/server/public/task-launcher.js +32 -5
  24. package/server/public/universal-launcher.js +26 -3
  25. package/server/socket.js +11 -22
  26. package/server/views/app.ejs +30 -167
  27. package/server/views/d.ejs +35 -33
  28. package/server/views/editor.ejs +4 -25
  29. package/server/views/partials/main_sidebar.ejs +0 -1
  30. package/server/views/partials/menu.ejs +1 -1
  31. package/server/views/pre.ejs +1 -1
  32. package/server/views/shell.ejs +3 -11
  33. package/server/views/task_launch.ejs +10 -10
  34. package/server/views/terminal.ejs +5 -34
  35. package/spec/INSTRUCTION_SYNC.md +5 -5
  36. package/kernel/agent_instructions.js +0 -166
  37. package/kernel/api/shell_run_template.js +0 -273
  38. package/kernel/api/uri/index.js +0 -51
  39. package/kernel/plugin_sources.js +0 -289
  40. package/kernel/watch/context.js +0 -42
  41. package/kernel/watch/drivers/fs.js +0 -71
  42. package/kernel/watch/drivers/poll.js +0 -33
  43. package/kernel/watch/index.js +0 -185
  44. package/server/features/index.js +0 -13
  45. package/server/features/notes/index.js +0 -41
  46. package/server/features/notes/parser.js +0 -174
  47. package/server/features/notes/public/notes.css +0 -955
  48. package/server/features/notes/public/notes.js +0 -1149
  49. package/server/features/notes/registry_import.js +0 -412
  50. package/server/features/notes/routes.js +0 -156
  51. package/server/features/notes/service.js +0 -326
  52. package/server/features/notes/watcher.js +0 -74
  53. package/server/lib/workspace_catalog.js +0 -151
  54. package/server/lib/workspace_runtime.js +0 -390
  55. package/server/public/tasker.css +0 -336
  56. package/server/public/tasker.js +0 -407
  57. package/server/routes/workspaces.js +0 -44
  58. package/server/views/partials/workspace_row.ejs +0 -61
  59. package/server/views/tasker.ejs +0 -40
  60. package/server/views/workspaces.ejs +0 -813
  61. package/system/plugin/antigravity/antigravity.png +0 -0
  62. package/system/plugin/antigravity/pinokio.js +0 -35
  63. package/system/plugin/claude/claude.png +0 -0
  64. package/system/plugin/claude/pinokio.js +0 -61
  65. package/system/plugin/claude-auto/claude.png +0 -0
  66. package/system/plugin/claude-auto/pinokio.js +0 -72
  67. package/system/plugin/claude-desktop/icon.jpeg +0 -0
  68. package/system/plugin/claude-desktop/pinokio.js +0 -37
  69. package/system/plugin/codex/openai.webp +0 -0
  70. package/system/plugin/codex/pinokio.js +0 -56
  71. package/system/plugin/codex-auto/openai.webp +0 -0
  72. package/system/plugin/codex-auto/pinokio.js +0 -63
  73. package/system/plugin/codex-desktop/icon.png +0 -0
  74. package/system/plugin/codex-desktop/pinokio.js +0 -37
  75. package/system/plugin/crush/crush.png +0 -0
  76. package/system/plugin/crush/pinokio.js +0 -29
  77. package/system/plugin/cursor/cursor.jpeg +0 -0
  78. package/system/plugin/cursor/pinokio.js +0 -37
  79. package/system/plugin/gemini/gemini.jpeg +0 -0
  80. package/system/plugin/gemini/pinokio.js +0 -38
  81. package/system/plugin/gemini-auto/gemini.jpeg +0 -0
  82. package/system/plugin/gemini-auto/pinokio.js +0 -41
  83. package/system/plugin/qwen/pinokio.js +0 -48
  84. package/system/plugin/qwen/qwen.png +0 -0
  85. package/system/plugin/vscode/pinokio.js +0 -34
  86. package/system/plugin/vscode/vscode.png +0 -0
  87. package/system/plugin/windsurf/pinokio.js +0 -37
  88. package/system/plugin/windsurf/windsurf.png +0 -0
  89. package/test/plugin-sources.test.js +0 -45
@@ -1,326 +0,0 @@
1
- const fs = require("fs")
2
- const path = require("path")
3
- const crypto = require("crypto")
4
- const {
5
- RESULT_RELATIVE_DIR,
6
- NOTE_FILENAME,
7
- METADATA_FILENAME,
8
- buildExcerpt,
9
- describeMediaRefs,
10
- extractTitle,
11
- parseNoteMetadata
12
- } = require("./parser")
13
-
14
- const MAX_MARKDOWN_BYTES = 5 * 1024 * 1024
15
-
16
- function createHash(value) {
17
- return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, 24)
18
- }
19
-
20
- function clonePlainObject(value) {
21
- if (!value || typeof value !== "object" || Array.isArray(value)) {
22
- return null
23
- }
24
- try {
25
- return JSON.parse(JSON.stringify(value))
26
- } catch (_) {
27
- return null
28
- }
29
- }
30
-
31
- function normalizeRelativePath(value, fallback) {
32
- const raw = String(value || fallback || "").trim().replace(/\\/g, "/")
33
- if (raw.startsWith("/") || /^[a-zA-Z]:/.test(raw)) {
34
- return fallback
35
- }
36
- const normalized = path.posix.normalize(raw)
37
- if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
38
- return fallback
39
- }
40
- return normalized
41
- }
42
-
43
- function isInside(candidate, parent) {
44
- const relative = path.relative(path.resolve(parent), path.resolve(candidate))
45
- return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
46
- }
47
-
48
- function normalizeNoteConfig(config = {}) {
49
- const params = config && typeof config === "object" ? config : {}
50
- return {
51
- path: normalizeRelativePath(params.path, RESULT_RELATIVE_DIR),
52
- content: normalizeRelativePath(params.content, ""),
53
- ready: normalizeRelativePath(params.ready, METADATA_FILENAME),
54
- description: typeof params.description === "string" ? params.description.trim() : "",
55
- publish: clonePlainObject(params.publish)
56
- }
57
- }
58
-
59
- async function findMetadataFiles(rootDir, filename) {
60
- const root = path.resolve(rootDir)
61
- const stats = await fs.promises.stat(root).catch(() => null)
62
- if (!stats || !stats.isDirectory()) {
63
- return []
64
- }
65
- const target = String(filename || METADATA_FILENAME)
66
- const results = []
67
- const visit = async (dir) => {
68
- const entries = await fs.promises.readdir(dir, { withFileTypes: true }).catch(() => null)
69
- if (!entries) return
70
- for (const entry of entries) {
71
- const fullPath = path.resolve(dir, entry.name)
72
- if (entry.isDirectory()) {
73
- await visit(fullPath)
74
- } else if (entry.isFile() && entry.name === target) {
75
- results.push(fullPath)
76
- }
77
- }
78
- }
79
- await visit(root)
80
- return results
81
- }
82
-
83
- function createNoteService({ kernel, taskWorkspaceLinks } = {}) {
84
- if (!kernel) {
85
- throw new Error("kernel is required")
86
- }
87
-
88
- const resultsByBundle = new Map()
89
-
90
- async function readMarkdown(notePath, stats) {
91
- const noteStats = stats || await fs.promises.stat(notePath)
92
- if (noteStats.size > MAX_MARKDOWN_BYTES) {
93
- throw new Error(`note markdown is too large (${noteStats.size} bytes)`)
94
- }
95
- return fs.promises.readFile(notePath, "utf8")
96
- }
97
-
98
- async function readNoteMetadata(metadataPath) {
99
- if (path.extname(metadataPath).toLowerCase() !== ".json") {
100
- return {}
101
- }
102
- const raw = await fs.promises.readFile(metadataPath, "utf8")
103
- return parseNoteMetadata(raw)
104
- }
105
-
106
- async function inspectNoteBundle({ taskId, ref, cwd, note, bundlePath, metadataPath } = {}) {
107
- if (typeof cwd !== "string" || !cwd.trim()) {
108
- return null
109
- }
110
- const workspacePath = path.resolve(cwd.trim())
111
- const noteConfig = normalizeNoteConfig(note)
112
- const rootDir = path.resolve(workspacePath, noteConfig.path)
113
- const resultDir = path.resolve(bundlePath || rootDir)
114
- const readyPath = path.resolve(metadataPath || path.resolve(resultDir, noteConfig.ready))
115
- const readyStats = await fs.promises.stat(readyPath).catch(() => null)
116
- if (!readyStats || !readyStats.isFile()) {
117
- resultsByBundle.delete(resultDir)
118
- return null
119
- }
120
- let metadata = {}
121
- try {
122
- metadata = await readNoteMetadata(readyPath)
123
- } catch (_) {
124
- resultsByBundle.delete(resultDir)
125
- return null
126
- }
127
-
128
- const contentPath = normalizeRelativePath(metadata.content || noteConfig.content, NOTE_FILENAME)
129
- const notePath = path.resolve(resultDir, contentPath)
130
- const noteStats = await fs.promises.stat(notePath).catch(() => null)
131
- if (!noteStats || !noteStats.isFile()) {
132
- resultsByBundle.delete(resultDir)
133
- return null
134
- }
135
-
136
- const markdown = await readMarkdown(notePath, noteStats)
137
- const workspaceName = path.basename(workspacePath)
138
- const bundleName = path.basename(resultDir)
139
- const media = await describeMediaRefs(markdown, resultDir)
140
- const updatedAtMs = Math.max(readyStats.mtimeMs || 0, noteStats.mtimeMs || 0)
141
- const id = createHash(`${workspacePath}|${resultDir}|${notePath}|${readyPath}`)
142
- const previous = resultsByBundle.get(resultDir) || null
143
- const publish = noteConfig.publish || (previous && previous.publish) || null
144
- const description = noteConfig.description || (previous && previous.description) || ""
145
- const mediaRevision = media
146
- .map((item) => `${item.ref}:${item.exists ? item.bytes : "missing"}:${item.mtimeMs || 0}`)
147
- .join("|")
148
- const revision = createHash(`${noteStats.size}|${noteStats.mtimeMs}|${readyStats.size}|${readyStats.mtimeMs}|${mediaRevision}`)
149
- const result = {
150
- id,
151
- revision,
152
- taskId,
153
- ref,
154
- cwd: workspacePath,
155
- workspaceName,
156
- bundleName,
157
- title: metadata.title || extractTitle(markdown, bundleName || workspaceName),
158
- markdown,
159
- excerpt: buildExcerpt(markdown),
160
- resultDir,
161
- watchRoot: rootDir,
162
- notePath,
163
- contentPath: notePath,
164
- readyPath,
165
- metadataPath: readyPath,
166
- metadata,
167
- publish,
168
- description,
169
- noteBytes: noteStats.size,
170
- media: media.map((item, index) => ({
171
- index,
172
- ref: item.ref,
173
- path: item.path,
174
- bytes: item.bytes,
175
- mtimeMs: item.mtimeMs,
176
- exists: item.exists,
177
- ext: path.extname(item.ref || "").toLowerCase()
178
- })),
179
- mediaCount: media.length,
180
- missingMediaCount: media.filter((item) => !item.exists).length,
181
- mediaBytes: media.reduce((total, item) => total + (Number.isFinite(item.bytes) ? item.bytes : 0), 0),
182
- updatedAt: new Date(updatedAtMs || Date.now()).toISOString()
183
- }
184
- resultsByBundle.set(resultDir, result)
185
- return result
186
- }
187
-
188
- async function inspectWorkspace({ taskId, ref, cwd, note } = {}) {
189
- if (typeof cwd !== "string" || !cwd.trim()) {
190
- return null
191
- }
192
- const workspacePath = path.resolve(cwd.trim())
193
- const noteConfig = normalizeNoteConfig(note)
194
- const rootDir = path.resolve(workspacePath, noteConfig.path)
195
- const metadataFiles = await findMetadataFiles(rootDir, noteConfig.ready)
196
- const seen = new Set()
197
- const results = []
198
- for (const metadataPath of metadataFiles) {
199
- const bundlePath = path.dirname(metadataPath)
200
- const result = await inspectNoteBundle({
201
- taskId,
202
- ref,
203
- cwd: workspacePath,
204
- note: noteConfig,
205
- bundlePath,
206
- metadataPath
207
- })
208
- if (result) {
209
- seen.add(result.resultDir)
210
- results.push(result)
211
- }
212
- }
213
- for (const [bundlePath, result] of Array.from(resultsByBundle.entries())) {
214
- if (result && result.cwd === workspacePath && result.watchRoot === rootDir && !seen.has(bundlePath)) {
215
- resultsByBundle.delete(bundlePath)
216
- }
217
- }
218
- return results[0] || null
219
- }
220
-
221
- async function trackWorkspace({ taskId, ref, cwd } = {}) {
222
- let resolvedCwd = cwd
223
- if (!resolvedCwd && ref && taskWorkspaceLinks && typeof taskWorkspaceLinks.resolveWorkspaceRef === "function") {
224
- resolvedCwd = taskWorkspaceLinks.resolveWorkspaceRef(ref)
225
- }
226
- if (!resolvedCwd) {
227
- return null
228
- }
229
- return inspectWorkspace({ taskId, ref, cwd: resolvedCwd })
230
- }
231
-
232
- async function listPending(options = {}) {
233
- const filterCwd = typeof options.cwd === "string" && options.cwd.trim()
234
- ? path.resolve(options.cwd.trim())
235
- : ""
236
- return Array.from(resultsByBundle.values())
237
- .filter((result) => !filterCwd || result.cwd === filterCwd)
238
- .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
239
- }
240
-
241
- async function getPendingById(id) {
242
- const normalizedId = typeof id === "string" ? id.trim() : ""
243
- if (!normalizedId) {
244
- return null
245
- }
246
- return Array.from(resultsByBundle.values()).find((item) => item.id === normalizedId) || null
247
- }
248
-
249
- async function refreshPendingItem(item) {
250
- if (!item || !item.cwd || !item.resultDir || !item.metadataPath) {
251
- return null
252
- }
253
- return inspectNoteBundle({
254
- taskId: item.taskId,
255
- ref: item.ref,
256
- cwd: item.cwd,
257
- note: {
258
- description: item.description,
259
- publish: item.publish
260
- },
261
- bundlePath: item.resultDir,
262
- metadataPath: item.metadataPath
263
- })
264
- }
265
-
266
- async function savePendingById(id, options = {}) {
267
- const item = await getPendingById(id)
268
- if (!item) {
269
- return null
270
- }
271
- const markdown = typeof options.markdown === "string" ? options.markdown : null
272
- if (markdown === null) {
273
- throw new Error("markdown is required")
274
- }
275
- if (Buffer.byteLength(markdown, "utf8") > MAX_MARKDOWN_BYTES) {
276
- const error = new Error(`note markdown is too large; limit is ${MAX_MARKDOWN_BYTES} bytes`)
277
- error.code = "NOTE_TOO_LARGE"
278
- throw error
279
- }
280
-
281
- const current = await refreshPendingItem(item)
282
- if (!current) {
283
- return null
284
- }
285
- const expectedRevision = typeof options.revision === "string" ? options.revision.trim() : ""
286
- if (expectedRevision && current.revision !== expectedRevision) {
287
- const error = new Error("Note changed on disk. Reload it before saving.")
288
- error.code = "NOTE_CONFLICT"
289
- error.item = current
290
- throw error
291
- }
292
-
293
- const notePath = path.resolve(current.notePath)
294
- const resultDir = path.resolve(current.resultDir)
295
- if (!isInside(notePath, resultDir)) {
296
- const error = new Error("Note path is outside the note folder")
297
- error.code = "NOTE_INVALID_PATH"
298
- throw error
299
- }
300
-
301
- await fs.promises.writeFile(notePath, markdown, "utf8")
302
- return refreshPendingItem(current)
303
- }
304
-
305
- async function start() {
306
- }
307
-
308
- async function stop() {
309
- }
310
-
311
- return {
312
- RESULT_RELATIVE_DIR,
313
- getPendingById,
314
- inspectNoteBundle,
315
- inspectWorkspace,
316
- listPending,
317
- savePendingById,
318
- start,
319
- stop,
320
- trackWorkspace
321
- }
322
- }
323
-
324
- module.exports = {
325
- createNoteService
326
- }
@@ -1,74 +0,0 @@
1
- const path = require("path")
2
-
3
- function isInside(candidate, parent) {
4
- const relative = path.relative(parent, candidate)
5
- return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
6
- }
7
-
8
- class NoteWatcher {
9
- constructor(options = {}) {
10
- this.notes = options.notes
11
- }
12
-
13
- async watch(ctx, params = {}) {
14
- if (!this.notes || typeof this.notes.inspectWorkspace !== "function") {
15
- throw new Error("note service is unavailable")
16
- }
17
-
18
- const noteDir = ctx.resolve(params.path || ".pinokio/notes")
19
- const noteConfig = {
20
- path: params.path || ".pinokio/notes",
21
- description: params.description,
22
- publish: params.publish
23
- }
24
- let timer = null
25
- let disposed = false
26
-
27
- const inspect = async () => {
28
- if (disposed) return
29
- await this.notes.inspectWorkspace({ cwd: ctx.cwd, note: noteConfig })
30
- }
31
-
32
- const scheduleInspect = () => {
33
- if (disposed) return
34
- if (timer) clearTimeout(timer)
35
- timer = setTimeout(() => {
36
- timer = null
37
- inspect().catch((error) => {
38
- console.warn("[notes] failed to inspect workspace", error && error.message ? error.message : error)
39
- })
40
- }, 250)
41
- }
42
-
43
- await inspect()
44
- const stopPoll = ctx.poll(params.interval || 1500, inspect, {
45
- immediate: false,
46
- onError: (error) => {
47
- console.warn("[notes] poll failed", error && error.message ? error.message : error)
48
- }
49
- })
50
- const unsubscribe = await ctx.watch.fs(ctx.cwd, (events) => {
51
- if (!Array.isArray(events)) return
52
- if (events.some((event) => event && event.path && isInside(path.resolve(event.path), noteDir))) {
53
- scheduleInspect()
54
- }
55
- })
56
-
57
- return async () => {
58
- if (timer) {
59
- clearTimeout(timer)
60
- timer = null
61
- }
62
- await this.notes.inspectWorkspace({ cwd: ctx.cwd, note: noteConfig }).catch(() => {})
63
- disposed = true
64
- if (typeof stopPoll === "function") {
65
- await stopPoll()
66
- }
67
- if (typeof unsubscribe === "function") {
68
- await unsubscribe()
69
- }
70
- }
71
- }
72
- }
73
-
74
- module.exports = NoteWatcher
@@ -1,151 +0,0 @@
1
- const fs = require("fs")
2
- const path = require("path")
3
-
4
- const SORT_MODES = new Set(["most_used", "last_opened", "az"])
5
-
6
- function normalizeSortMode(sort) {
7
- if (SORT_MODES.has(sort)) return sort
8
- return "most_used"
9
- }
10
-
11
- function normalizePathKey(filepath) {
12
- const resolved = path.resolve(filepath).replace(/[\\/]+$/, "")
13
- return process.platform === "win32" ? resolved.toLowerCase() : resolved
14
- }
15
-
16
- function toRoutePath(filepath) {
17
- const resolved = path.resolve(filepath).replace(/\\/g, "/")
18
- const encoded = resolved
19
- .split("/")
20
- .map((segment, index) => {
21
- if (index === 0 && segment === "") return ""
22
- return encodeURIComponent(segment)
23
- })
24
- .join("/")
25
- return encoded.startsWith("/") ? `/d${encoded}` : `/d/${encoded}`
26
- }
27
-
28
- function latestTimestamp(values) {
29
- return values.reduce((latest, value) => {
30
- if (!value) return latest
31
- const timestamp = typeof value === "number" ? value : new Date(value).getTime()
32
- return Number.isFinite(timestamp) && timestamp > latest ? timestamp : latest
33
- }, 0)
34
- }
35
-
36
- function sortWorkspaces(items, sort) {
37
- const mode = normalizeSortMode(sort)
38
- const sorted = [...items]
39
- sorted.sort((a, b) => {
40
- if (mode === "az") {
41
- return a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
42
- }
43
- if (mode === "last_opened") {
44
- const delta = (b.lastOpenedAtMs || b.modifiedAtMs || 0) - (a.lastOpenedAtMs || a.modifiedAtMs || 0)
45
- if (delta !== 0) return delta
46
- return a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
47
- }
48
- const usageDelta = (b.usageCount || 0) - (a.usageCount || 0)
49
- if (usageDelta !== 0) return usageDelta
50
- return a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
51
- })
52
- return sorted
53
- }
54
-
55
- function newestNote(notes) {
56
- return [...notes].sort((a, b) => {
57
- return (new Date(b.updatedAt || 0).getTime() || 0) - (new Date(a.updatedAt || 0).getTime() || 0)
58
- })[0] || null
59
- }
60
-
61
- function createWorkspaceCatalogService({ kernel, workspaceRuntime, notes }) {
62
- async function list(options = {}) {
63
- const sort = normalizeSortMode(options.sort)
64
- const root = path.resolve(kernel.path("workspaces"))
65
- const entries = await fs.promises.readdir(root, { withFileTypes: true }).catch(() => [])
66
- const runtime = workspaceRuntime.list()
67
- const liveByPath = new Map()
68
-
69
- for (const group of runtime.workspaces || []) {
70
- if (group.root !== "workspaces") continue
71
- liveByPath.set(normalizePathKey(group.cwd), group)
72
- }
73
-
74
- const noteByPath = new Map()
75
- const pendingNotes = notes ? await notes.listPending({}).catch(() => []) : []
76
- for (const note of pendingNotes) {
77
- if (!note.cwd) continue
78
- const key = normalizePathKey(note.cwd)
79
- const list = noteByPath.get(key) || []
80
- list.push(note)
81
- noteByPath.set(key, list)
82
- }
83
-
84
- const folders = entries.filter((entry) => entry.isDirectory())
85
- const items = []
86
-
87
- for (const entry of folders) {
88
- const cwd = path.join(root, entry.name)
89
- const stats = await fs.promises.stat(cwd).catch(() => null)
90
- const key = normalizePathKey(cwd)
91
- const live = liveByPath.get(key)
92
- const shells = live?.shells || []
93
- const scripts = live?.scripts || []
94
- const workspaceNotes = noteByPath.get(key) || []
95
- const note = newestNote(workspaceNotes)
96
- const modifiedAtMs = stats?.mtimeMs || 0
97
- const lastOpenedAtMs = latestTimestamp([
98
- modifiedAtMs,
99
- ...shells.map((shell) => shell.start_time),
100
- ...workspaceNotes.map((item) => item.updatedAt),
101
- ])
102
- const primaryShell = shells.length === 1 ? shells[0] : null
103
- const primaryScript = scripts.length === 1 ? scripts[0] : null
104
- const usageCount = shells.length + scripts.length
105
-
106
- items.push({
107
- name: entry.name,
108
- cwd,
109
- relpath: entry.name,
110
- modifiedAt: modifiedAtMs ? new Date(modifiedAtMs).toISOString() : null,
111
- modifiedAtMs,
112
- lastOpenedAt: lastOpenedAtMs ? new Date(lastOpenedAtMs).toISOString() : null,
113
- lastOpenedAtMs,
114
- usageCount,
115
- running: shells.length > 0 || scripts.length > 0,
116
- counts: {
117
- shells: shells.length,
118
- scripts: scripts.length,
119
- notes: workspaceNotes.length,
120
- },
121
- shells,
122
- scripts,
123
- note,
124
- noteReady: Boolean(note),
125
- primaryUrl: primaryScript?.url || primaryShell?.url || null,
126
- launchUrl: toRoutePath(cwd),
127
- })
128
- }
129
-
130
- const running = sortWorkspaces(items.filter((item) => item.running), sort)
131
- const offline = sortWorkspaces(items.filter((item) => !item.running), sort)
132
-
133
- return {
134
- root,
135
- sort,
136
- running,
137
- offline,
138
- items: [...running, ...offline],
139
- counts: {
140
- total: items.length,
141
- running: running.length,
142
- offline: offline.length,
143
- notes: items.filter((item) => item.noteReady).length,
144
- },
145
- }
146
- }
147
-
148
- return { list, normalizeSortMode }
149
- }
150
-
151
- module.exports = { createWorkspaceCatalogService, normalizeSortMode }