pinokiod 6.0.19 → 6.0.21
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/kernel/environment.js +55 -1
- package/kernel/shell.js +3 -1
- package/package.json +1 -1
- package/server/index.js +871 -101
- package/server/public/common.js +7 -0
- package/server/public/install.js +1 -1
- package/server/public/style.css +4 -5
- package/server/public/terminal-settings.js +1 -1
- package/server/scripts/fork_gemini_session.js +67 -0
- package/server/scripts/gemini_fork_and_resume.js +50 -0
- package/server/views/app.ejs +80 -5
- package/server/views/bootstrap.ejs +1 -1
- package/server/views/editor.ejs +1 -1
- package/server/views/explore.ejs +76 -1
- package/server/views/index.ejs +3 -3
- package/server/views/init/index.ejs +1 -1
- package/server/views/install.ejs +1 -1
- package/server/views/net.ejs +1 -1
- package/server/views/pro.ejs +1 -1
- package/server/views/prototype/index.ejs +1 -1
- package/server/views/shell.ejs +2 -2
- package/server/views/terminal.ejs +1 -1
- package/server/views/terminals.ejs +1359 -141
package/server/index.js
CHANGED
|
@@ -4269,6 +4269,7 @@ class Server {
|
|
|
4269
4269
|
|
|
4270
4270
|
|
|
4271
4271
|
await this.kernel.init({ port: this.port})
|
|
4272
|
+
await Environment.init({}, this.kernel)
|
|
4272
4273
|
this.kernel.server_port = this.port
|
|
4273
4274
|
this.kernel.peer.start(this.kernel)
|
|
4274
4275
|
|
|
@@ -5731,6 +5732,94 @@ class Server {
|
|
|
5731
5732
|
const invalidateTerminalSessionDiscoveryCache = () => {
|
|
5732
5733
|
terminalSessionDiscoveryCache.expires = 0
|
|
5733
5734
|
}
|
|
5735
|
+
let terminalSessionRegistrySyncPromise = null
|
|
5736
|
+
let terminalSessionRegistryBootScrubbed = false
|
|
5737
|
+
let terminalSessionRegistryBootScrubPromise = null
|
|
5738
|
+
|
|
5739
|
+
const getTerminalSessionRegistryPath = () => {
|
|
5740
|
+
if (this.kernel && typeof this.kernel.path === "function") {
|
|
5741
|
+
return this.kernel.path("cache", "terminals", "sessions.json")
|
|
5742
|
+
}
|
|
5743
|
+
return path.resolve(os.homedir(), "pinokio", "cache", "terminals", "sessions.json")
|
|
5744
|
+
}
|
|
5745
|
+
const coerceTerminalRegistryItems = (items) => {
|
|
5746
|
+
if (!Array.isArray(items)) {
|
|
5747
|
+
return []
|
|
5748
|
+
}
|
|
5749
|
+
return items.filter((entry) => entry && typeof entry === "object")
|
|
5750
|
+
}
|
|
5751
|
+
const terminalRegistryItemsEqual = (left, right) => {
|
|
5752
|
+
return JSON.stringify(coerceTerminalRegistryItems(left)) === JSON.stringify(coerceTerminalRegistryItems(right))
|
|
5753
|
+
}
|
|
5754
|
+
const readTerminalSessionRegistry = async () => {
|
|
5755
|
+
const registryPath = getTerminalSessionRegistryPath()
|
|
5756
|
+
try {
|
|
5757
|
+
const raw = await fs.promises.readFile(registryPath, "utf8")
|
|
5758
|
+
const parsed = JSON.parse(raw)
|
|
5759
|
+
if (!parsed || typeof parsed !== "object") {
|
|
5760
|
+
return { items: [], exists: false }
|
|
5761
|
+
}
|
|
5762
|
+
return {
|
|
5763
|
+
items: coerceTerminalRegistryItems(parsed.items),
|
|
5764
|
+
exists: true
|
|
5765
|
+
}
|
|
5766
|
+
} catch (error) {
|
|
5767
|
+
return { items: [], exists: false }
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
const writeTerminalSessionRegistry = async (items) => {
|
|
5771
|
+
const registryPath = getTerminalSessionRegistryPath()
|
|
5772
|
+
const normalizedItems = coerceTerminalRegistryItems(items)
|
|
5773
|
+
const payload = {
|
|
5774
|
+
updated_at: new Date().toISOString(),
|
|
5775
|
+
items: normalizedItems
|
|
5776
|
+
}
|
|
5777
|
+
await fs.promises.mkdir(path.dirname(registryPath), { recursive: true })
|
|
5778
|
+
const tmpPath = `${registryPath}.tmp`
|
|
5779
|
+
await fs.promises.writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf8")
|
|
5780
|
+
await fs.promises.rename(tmpPath, registryPath)
|
|
5781
|
+
return payload
|
|
5782
|
+
}
|
|
5783
|
+
const scrubTerminalSessionRegistryOnlineStateAtBoot = async () => {
|
|
5784
|
+
if (terminalSessionRegistryBootScrubbed) {
|
|
5785
|
+
return
|
|
5786
|
+
}
|
|
5787
|
+
if (!terminalSessionRegistryBootScrubPromise) {
|
|
5788
|
+
terminalSessionRegistryBootScrubPromise = (async () => {
|
|
5789
|
+
const registry = await readTerminalSessionRegistry()
|
|
5790
|
+
if (!registry.exists || !Array.isArray(registry.items) || registry.items.length === 0) {
|
|
5791
|
+
terminalSessionRegistryBootScrubbed = true
|
|
5792
|
+
return
|
|
5793
|
+
}
|
|
5794
|
+
let changed = false
|
|
5795
|
+
const scrubbedItems = registry.items.map((entry) => {
|
|
5796
|
+
if (!entry || typeof entry !== "object") {
|
|
5797
|
+
return entry
|
|
5798
|
+
}
|
|
5799
|
+
const onlineValue = entry.online
|
|
5800
|
+
const isOnline = onlineValue === true
|
|
5801
|
+
|| onlineValue === 1
|
|
5802
|
+
|| onlineValue === "1"
|
|
5803
|
+
|| onlineValue === "true"
|
|
5804
|
+
if (!isOnline) {
|
|
5805
|
+
return entry
|
|
5806
|
+
}
|
|
5807
|
+
changed = true
|
|
5808
|
+
return {
|
|
5809
|
+
...entry,
|
|
5810
|
+
online: false
|
|
5811
|
+
}
|
|
5812
|
+
})
|
|
5813
|
+
if (changed) {
|
|
5814
|
+
await writeTerminalSessionRegistry(scrubbedItems)
|
|
5815
|
+
}
|
|
5816
|
+
terminalSessionRegistryBootScrubbed = true
|
|
5817
|
+
})().finally(() => {
|
|
5818
|
+
terminalSessionRegistryBootScrubPromise = null
|
|
5819
|
+
})
|
|
5820
|
+
}
|
|
5821
|
+
return terminalSessionRegistryBootScrubPromise
|
|
5822
|
+
}
|
|
5734
5823
|
|
|
5735
5824
|
const getTerminalSkillRoots = () => {
|
|
5736
5825
|
const roots = []
|
|
@@ -6054,6 +6143,45 @@ class Server {
|
|
|
6054
6143
|
return `${lines.join("\n").trim()}\n`
|
|
6055
6144
|
}
|
|
6056
6145
|
|
|
6146
|
+
const buildCodexSelectedSkillMarkdown = (mergedBody) => {
|
|
6147
|
+
const body = String(mergedBody || "").trim()
|
|
6148
|
+
const lines = [
|
|
6149
|
+
"---",
|
|
6150
|
+
"name: Pinokio Selected Skills",
|
|
6151
|
+
"description: Session-specific skill bundle generated from the skills selected in Pinokio.",
|
|
6152
|
+
"tags:",
|
|
6153
|
+
"- pinokio",
|
|
6154
|
+
"- session",
|
|
6155
|
+
"- selected-skills",
|
|
6156
|
+
"---",
|
|
6157
|
+
"",
|
|
6158
|
+
body
|
|
6159
|
+
]
|
|
6160
|
+
return `${lines.join("\n").trim()}\n`
|
|
6161
|
+
}
|
|
6162
|
+
|
|
6163
|
+
const ensureCodexSelectedSkillFrontmatter = async (sessionCwd) => {
|
|
6164
|
+
if (typeof sessionCwd !== "string" || sessionCwd.trim().length === 0) {
|
|
6165
|
+
return
|
|
6166
|
+
}
|
|
6167
|
+
const codexSkillPath = path.resolve(sessionCwd, ".agents", "skills", "pinokio-selected", "SKILL.md")
|
|
6168
|
+
let existing = ""
|
|
6169
|
+
try {
|
|
6170
|
+
existing = await fs.promises.readFile(codexSkillPath, "utf8")
|
|
6171
|
+
} catch (error) {
|
|
6172
|
+
if (error && error.code === "ENOENT") {
|
|
6173
|
+
return
|
|
6174
|
+
}
|
|
6175
|
+
throw error
|
|
6176
|
+
}
|
|
6177
|
+
const normalized = String(existing || "").replace(/\r\n/g, "\n")
|
|
6178
|
+
if (normalized.startsWith("---\n")) {
|
|
6179
|
+
return
|
|
6180
|
+
}
|
|
6181
|
+
const wrapped = buildCodexSelectedSkillMarkdown(normalized)
|
|
6182
|
+
await fs.promises.writeFile(codexSkillPath, wrapped, "utf8")
|
|
6183
|
+
}
|
|
6184
|
+
|
|
6057
6185
|
const materializeTerminalSkillContext = async (sessionCwd, providerKey, selectedSkills) => {
|
|
6058
6186
|
if (!Array.isArray(selectedSkills) || selectedSkills.length === 0) {
|
|
6059
6187
|
return {
|
|
@@ -6102,7 +6230,8 @@ class Server {
|
|
|
6102
6230
|
const codexSkillDir = path.resolve(sessionCwd, ".agents", "skills", "pinokio-selected")
|
|
6103
6231
|
await fs.promises.mkdir(codexSkillDir, { recursive: true })
|
|
6104
6232
|
const codexSkillPath = path.resolve(codexSkillDir, "SKILL.md")
|
|
6105
|
-
|
|
6233
|
+
const codexSkillBody = buildCodexSelectedSkillMarkdown(merged)
|
|
6234
|
+
await fs.promises.writeFile(codexSkillPath, codexSkillBody, "utf8")
|
|
6106
6235
|
const agentsPath = path.resolve(sessionCwd, "AGENTS.md")
|
|
6107
6236
|
await fs.promises.writeFile(agentsPath, "# Pinokio session instructions\n\nUse the `pinokio-selected` skill from `.agents/skills/pinokio-selected/SKILL.md` for this workspace.\n", "utf8")
|
|
6108
6237
|
} else if (providerKey === "claude") {
|
|
@@ -6324,7 +6453,10 @@ class Server {
|
|
|
6324
6453
|
const msg = candidate.messages[i]
|
|
6325
6454
|
if (msg && typeof msg === "object") {
|
|
6326
6455
|
const role = typeof msg.role === "string" ? msg.role.toLowerCase() : ""
|
|
6327
|
-
|
|
6456
|
+
const type = typeof msg.type === "string" ? msg.type.toLowerCase() : ""
|
|
6457
|
+
const author = typeof msg.author === "string" ? msg.author.toLowerCase() : ""
|
|
6458
|
+
const isUserMessage = role === "user" || type === "user" || author === "user" || author === "human"
|
|
6459
|
+
if (isUserMessage) {
|
|
6328
6460
|
const text = trimSummary(extractTextContent(msg), 160)
|
|
6329
6461
|
if (text) {
|
|
6330
6462
|
return text
|
|
@@ -6416,7 +6548,8 @@ class Server {
|
|
|
6416
6548
|
return normalizeClaudeSummary(fallback)
|
|
6417
6549
|
}
|
|
6418
6550
|
|
|
6419
|
-
const buildTerminalSessions = async (forceDiscovery = false) => {
|
|
6551
|
+
const buildTerminalSessions = async (forceDiscovery = false, options = {}) => {
|
|
6552
|
+
const cacheOnly = Boolean(options && options.cacheOnly)
|
|
6420
6553
|
const buildCodexResumeBaseCommand = (entry) => {
|
|
6421
6554
|
const defaultCommand = entry && entry.command ? entry.command : "npx -y @openai/codex@latest"
|
|
6422
6555
|
return {
|
|
@@ -6815,31 +6948,38 @@ class Server {
|
|
|
6815
6948
|
}
|
|
6816
6949
|
seenNodes.add(candidate)
|
|
6817
6950
|
const localCodexHint = inheritedHint || recordHasCodexHint(candidate, 2, 50)
|
|
6818
|
-
const
|
|
6951
|
+
const candidateType = typeof candidate.type === "string" ? candidate.type.toLowerCase() : ""
|
|
6952
|
+
// OpenClaw session logs include many message/tool-call ids. Only treat top-level
|
|
6953
|
+
// session envelopes as resumable session sources.
|
|
6954
|
+
const isOpenClawSessionEnvelope = candidateType === "session" || candidateType === "session_meta"
|
|
6955
|
+
const sessionField = isOpenClawSessionEnvelope ? findSessionField(candidate) : null
|
|
6819
6956
|
if (sessionField && isCodexFieldAllowed(sessionField, candidate, filePath, localCodexHint)) {
|
|
6820
6957
|
const sessionId = sessionField.value
|
|
6821
6958
|
if (!seenIds.has(sessionId)) {
|
|
6822
|
-
|
|
6823
|
-
const
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
candidate
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6959
|
+
const cwdValue = extractWorkingDirectory(candidate) || extractWorkingDirectory(record)
|
|
6960
|
+
const cwd = typeof cwdValue === "string" ? cwdValue.trim() : ""
|
|
6961
|
+
if (cwd) {
|
|
6962
|
+
seenIds.add(sessionId)
|
|
6963
|
+
const summary = normalizeDiscoveredSessionSummary(buildSessionSummary(candidate) || buildSessionSummary(record))
|
|
6964
|
+
const timestamp = parseSessionTimestamp(
|
|
6965
|
+
candidate.timestamp
|
|
6966
|
+
|| candidate.ts
|
|
6967
|
+
|| candidate.updated_at
|
|
6968
|
+
|| candidate.created_at
|
|
6969
|
+
|| record.timestamp
|
|
6970
|
+
|| record.ts
|
|
6971
|
+
|| record.updated_at
|
|
6972
|
+
|| record.created_at
|
|
6973
|
+
)
|
|
6974
|
+
results.push({
|
|
6975
|
+
id: sessionId,
|
|
6976
|
+
cwd,
|
|
6977
|
+
summary,
|
|
6978
|
+
timestamp,
|
|
6979
|
+
source: filePath,
|
|
6980
|
+
metadata: candidate
|
|
6981
|
+
})
|
|
6982
|
+
}
|
|
6843
6983
|
}
|
|
6844
6984
|
}
|
|
6845
6985
|
if (depth >= maxDepth) {
|
|
@@ -6950,6 +7090,7 @@ class Server {
|
|
|
6950
7090
|
}
|
|
6951
7091
|
|
|
6952
7092
|
const home = os.homedir()
|
|
7093
|
+
const configuredHome = this.kernel && this.kernel.homedir ? path.resolve(this.kernel.homedir) : null
|
|
6953
7094
|
const openClawAgentSessionRoots = await buildOpenClawAgentSessionRoots(home)
|
|
6954
7095
|
const codexDiscovery = await buildCodexDiscovery(home, openClawAgentSessionRoots)
|
|
6955
7096
|
const codexDiscoveryRoots = codexDiscovery.roots
|
|
@@ -6975,10 +7116,78 @@ class Server {
|
|
|
6975
7116
|
...buildClaudeDiscoveryRoots(home),
|
|
6976
7117
|
...openClawAgentSessionRoots
|
|
6977
7118
|
])
|
|
6978
|
-
const
|
|
6979
|
-
|
|
6980
|
-
|
|
7119
|
+
const geminiHomeRoots = normalizeDiscoveryRoots([
|
|
7120
|
+
home,
|
|
7121
|
+
configuredHome
|
|
6981
7122
|
])
|
|
7123
|
+
const geminiRootCandidates = []
|
|
7124
|
+
for (let i = 0; i < geminiHomeRoots.length; i++) {
|
|
7125
|
+
const rootHome = geminiHomeRoots[i]
|
|
7126
|
+
geminiRootCandidates.push(path.join(rootHome, ".gemini", "tmp"))
|
|
7127
|
+
geminiRootCandidates.push(path.join(rootHome, ".config", "gemini", "tmp"))
|
|
7128
|
+
}
|
|
7129
|
+
const geminiCanonicalRoots = normalizeDiscoveryRoots(geminiRootCandidates)
|
|
7130
|
+
const geminiProjectRootCache = new Map()
|
|
7131
|
+
const getGeminiCanonicalTokenFromFile = (filePath) => {
|
|
7132
|
+
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
7133
|
+
return null
|
|
7134
|
+
}
|
|
7135
|
+
const resolved = path.resolve(filePath)
|
|
7136
|
+
for (let i = 0; i < geminiCanonicalRoots.length; i++) {
|
|
7137
|
+
const root = geminiCanonicalRoots[i]
|
|
7138
|
+
if (!root) {
|
|
7139
|
+
continue
|
|
7140
|
+
}
|
|
7141
|
+
const resolvedRoot = path.resolve(root)
|
|
7142
|
+
if (!(resolved === resolvedRoot || resolved.startsWith(`${resolvedRoot}${path.sep}`))) {
|
|
7143
|
+
continue
|
|
7144
|
+
}
|
|
7145
|
+
const relative = path.relative(resolvedRoot, resolved)
|
|
7146
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
7147
|
+
continue
|
|
7148
|
+
}
|
|
7149
|
+
const token = normalizeSessionToken(relative.split(path.sep)[0])
|
|
7150
|
+
if (token) {
|
|
7151
|
+
return token
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
return null
|
|
7155
|
+
}
|
|
7156
|
+
const resolveGeminiProjectRoot = (filePath) => {
|
|
7157
|
+
const token = getGeminiCanonicalTokenFromFile(filePath)
|
|
7158
|
+
if (!token) {
|
|
7159
|
+
return null
|
|
7160
|
+
}
|
|
7161
|
+
if (geminiProjectRootCache.has(token)) {
|
|
7162
|
+
const cached = geminiProjectRootCache.get(token)
|
|
7163
|
+
return cached || null
|
|
7164
|
+
}
|
|
7165
|
+
let projectRoot = null
|
|
7166
|
+
for (let i = 0; i < geminiCanonicalRoots.length; i++) {
|
|
7167
|
+
const root = geminiCanonicalRoots[i]
|
|
7168
|
+
if (!root) {
|
|
7169
|
+
continue
|
|
7170
|
+
}
|
|
7171
|
+
const markerPath = path.resolve(root, token, ".project_root")
|
|
7172
|
+
try {
|
|
7173
|
+
const marker = fs.readFileSync(markerPath, "utf8")
|
|
7174
|
+
const normalized = typeof marker === "string" ? marker.trim() : ""
|
|
7175
|
+
if (normalized.length > 0) {
|
|
7176
|
+
projectRoot = normalized
|
|
7177
|
+
break
|
|
7178
|
+
}
|
|
7179
|
+
} catch (error) {
|
|
7180
|
+
}
|
|
7181
|
+
}
|
|
7182
|
+
if (!projectRoot && this.kernel && typeof this.kernel.path === "function") {
|
|
7183
|
+
const inferredPath = path.resolve(this.kernel.path("terminals", "gemini", token))
|
|
7184
|
+
if (fs.existsSync(inferredPath)) {
|
|
7185
|
+
projectRoot = inferredPath
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
geminiProjectRootCache.set(token, projectRoot || "")
|
|
7189
|
+
return projectRoot
|
|
7190
|
+
}
|
|
6982
7191
|
const isGeminiCanonicalSessionFile = (filePath) => {
|
|
6983
7192
|
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
6984
7193
|
return false
|
|
@@ -6991,6 +7200,18 @@ class Server {
|
|
|
6991
7200
|
}
|
|
6992
7201
|
return /[\\/]chats[\\/]session-[^\\/]+\.json$/i.test(path.resolve(filePath))
|
|
6993
7202
|
}
|
|
7203
|
+
const isGeminiCanonicalLogFile = (filePath) => {
|
|
7204
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
7205
|
+
return false
|
|
7206
|
+
}
|
|
7207
|
+
if (isOpenClawDiscoveryPath(filePath)) {
|
|
7208
|
+
return false
|
|
7209
|
+
}
|
|
7210
|
+
if (!isPathWithinRoots(filePath, geminiCanonicalRoots)) {
|
|
7211
|
+
return false
|
|
7212
|
+
}
|
|
7213
|
+
return /[\\/]logs\.json$/i.test(path.resolve(filePath))
|
|
7214
|
+
}
|
|
6994
7215
|
const isGeminiForkCapableRecord = (candidate, filePath) => {
|
|
6995
7216
|
if (!candidate || typeof candidate !== "object") {
|
|
6996
7217
|
return false
|
|
@@ -7121,7 +7342,7 @@ class Server {
|
|
|
7121
7342
|
if (isOpenClawDiscoveryPath(filePath)) {
|
|
7122
7343
|
return true
|
|
7123
7344
|
}
|
|
7124
|
-
return isGeminiCanonicalSessionFile(filePath)
|
|
7345
|
+
return isGeminiCanonicalSessionFile(filePath) || isGeminiCanonicalLogFile(filePath)
|
|
7125
7346
|
},
|
|
7126
7347
|
extract: (record, filePath) => {
|
|
7127
7348
|
if (!record || typeof record !== "object") {
|
|
@@ -7140,10 +7361,35 @@ class Server {
|
|
|
7140
7361
|
}))
|
|
7141
7362
|
}
|
|
7142
7363
|
}
|
|
7364
|
+
const candidate = record.payload && typeof record.payload === "object" ? record.payload : record
|
|
7365
|
+
if (isGeminiCanonicalLogFile(filePath)) {
|
|
7366
|
+
const sessionField = findSessionField(candidate, geminiSessionIdKeys)
|
|
7367
|
+
const sessionId = sessionField ? sessionField.value : null
|
|
7368
|
+
if (!sessionId) {
|
|
7369
|
+
return null
|
|
7370
|
+
}
|
|
7371
|
+
const recordType = typeof candidate.type === "string" ? candidate.type.toLowerCase() : ""
|
|
7372
|
+
const isUserRecord = recordType === "user"
|
|
7373
|
+
const summary = normalizeDiscoveredSessionSummary(
|
|
7374
|
+
isUserRecord
|
|
7375
|
+
? extractTextContent(candidate.message || candidate.content || candidate.prompt || candidate.text)
|
|
7376
|
+
: buildSessionSummary(candidate)
|
|
7377
|
+
)
|
|
7378
|
+
return {
|
|
7379
|
+
id: sessionId,
|
|
7380
|
+
cwd: extractWorkingDirectory(candidate) || resolveGeminiProjectRoot(filePath),
|
|
7381
|
+
summary,
|
|
7382
|
+
timestamp: parseSessionTimestamp(candidate.timestamp || candidate.ts || candidate.lastUpdated || candidate.startTime || record.timestamp || record.ts),
|
|
7383
|
+
source: filePath,
|
|
7384
|
+
source_kind: "gemini_log",
|
|
7385
|
+
resume_capable: false,
|
|
7386
|
+
resume_disabled_reason: "Gemini log-only session cannot be resumed directly.",
|
|
7387
|
+
fork_capable: false
|
|
7388
|
+
}
|
|
7389
|
+
}
|
|
7143
7390
|
if (!isGeminiCanonicalSessionFile(filePath)) {
|
|
7144
7391
|
return null
|
|
7145
7392
|
}
|
|
7146
|
-
const candidate = record.payload && typeof record.payload === "object" ? record.payload : record
|
|
7147
7393
|
const sessionField = findSessionField(candidate, geminiSessionIdKeys)
|
|
7148
7394
|
const sessionId = sessionField ? sessionField.value : null
|
|
7149
7395
|
if (!sessionId) {
|
|
@@ -7151,13 +7397,15 @@ class Server {
|
|
|
7151
7397
|
}
|
|
7152
7398
|
const rootSessionId = normalizeSessionIdentifier(candidate.sessionId || candidate.session_id)
|
|
7153
7399
|
const forkCapable = isGeminiForkCapableRecord(candidate, filePath) && rootSessionId === sessionId
|
|
7154
|
-
const summary = buildSessionSummary(candidate)
|
|
7400
|
+
const summary = normalizeDiscoveredSessionSummary(buildSessionSummary(candidate))
|
|
7155
7401
|
return {
|
|
7156
7402
|
id: sessionId,
|
|
7157
|
-
cwd: extractWorkingDirectory(candidate),
|
|
7403
|
+
cwd: extractWorkingDirectory(candidate) || resolveGeminiProjectRoot(filePath),
|
|
7158
7404
|
summary,
|
|
7159
7405
|
timestamp: parseSessionTimestamp(candidate.timestamp || candidate.ts || candidate.lastUpdated || candidate.startTime || record.timestamp || record.ts),
|
|
7160
7406
|
source: filePath,
|
|
7407
|
+
source_kind: "gemini_session",
|
|
7408
|
+
resume_capable: true,
|
|
7161
7409
|
fork_capable: forkCapable
|
|
7162
7410
|
}
|
|
7163
7411
|
}
|
|
@@ -7290,6 +7538,43 @@ class Server {
|
|
|
7290
7538
|
}
|
|
7291
7539
|
return Array.from(keySet.values())
|
|
7292
7540
|
}
|
|
7541
|
+
const inferProviderFromCommand = (commandText) => {
|
|
7542
|
+
if (typeof commandText !== "string" || commandText.trim().length === 0) {
|
|
7543
|
+
return ""
|
|
7544
|
+
}
|
|
7545
|
+
for (let i = 0; i < providerKeys.length; i++) {
|
|
7546
|
+
const key = providerKeys[i]
|
|
7547
|
+
const terms = getProviderHintTerms(key)
|
|
7548
|
+
if (stringHasHintTerm(commandText, terms)) {
|
|
7549
|
+
return key
|
|
7550
|
+
}
|
|
7551
|
+
}
|
|
7552
|
+
return ""
|
|
7553
|
+
}
|
|
7554
|
+
const parseSessionTokensFromCommand = (commandText) => {
|
|
7555
|
+
if (typeof commandText !== "string" || commandText.trim().length === 0) {
|
|
7556
|
+
return []
|
|
7557
|
+
}
|
|
7558
|
+
const tokens = new Set()
|
|
7559
|
+
const patterns = [
|
|
7560
|
+
/--resume(?:=|\s+)(?:"([^"]+)"|'([^']+)'|([^\s"'`]+))/gi,
|
|
7561
|
+
/\bresume\s+(?:"([^"]+)"|'([^']+)'|([^\s"'`]+))/gi,
|
|
7562
|
+
/\bfork\s+(?:"([^"]+)"|'([^']+)'|([^\s"'`]+))/gi,
|
|
7563
|
+
/session=(?:"([^"]+)"|'([^']+)'|([^\s"'`&]+))/gi
|
|
7564
|
+
]
|
|
7565
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
7566
|
+
const pattern = patterns[i]
|
|
7567
|
+
let match = null
|
|
7568
|
+
while ((match = pattern.exec(commandText)) !== null) {
|
|
7569
|
+
const rawToken = match[1] || match[2] || match[3] || ""
|
|
7570
|
+
const normalized = normalizeSessionToken(rawToken)
|
|
7571
|
+
if (normalized) {
|
|
7572
|
+
tokens.add(normalized)
|
|
7573
|
+
}
|
|
7574
|
+
}
|
|
7575
|
+
}
|
|
7576
|
+
return Array.from(tokens.values())
|
|
7577
|
+
}
|
|
7293
7578
|
const normalizeCwdKey = (value) => {
|
|
7294
7579
|
if (typeof value !== "string") {
|
|
7295
7580
|
return ""
|
|
@@ -7305,7 +7590,10 @@ class Server {
|
|
|
7305
7590
|
return normalized
|
|
7306
7591
|
}
|
|
7307
7592
|
const runningSessionKeys = new Set()
|
|
7593
|
+
const runningProviderCwdKeys = new Set()
|
|
7308
7594
|
const runningForkProviderCwdKeys = new Set()
|
|
7595
|
+
const runningShellIdsByTerminalId = new Map()
|
|
7596
|
+
const runningShellIdByTerminalSession = new Map()
|
|
7309
7597
|
if (this.kernel && this.kernel.api && this.kernel.api.running) {
|
|
7310
7598
|
for (const runningId of Object.keys(this.kernel.api.running)) {
|
|
7311
7599
|
const keys = parseRunningSessionKeys(runningId)
|
|
@@ -7329,9 +7617,27 @@ class Server {
|
|
|
7329
7617
|
}
|
|
7330
7618
|
if (this.kernel && this.kernel.shell && Array.isArray(this.kernel.shell.shells)) {
|
|
7331
7619
|
for (const shellEntry of this.kernel.shell.shells) {
|
|
7620
|
+
const shellIdText = shellEntry && shellEntry.id ? String(shellEntry.id) : ""
|
|
7621
|
+
if (shellIdText) {
|
|
7622
|
+
const querySeparatorIndex = shellIdText.indexOf("?")
|
|
7623
|
+
if (querySeparatorIndex >= 0) {
|
|
7624
|
+
const normalizedQuery = shellIdText.slice(querySeparatorIndex + 1).replace(/&/g, "&")
|
|
7625
|
+
const shellParams = new URLSearchParams(normalizedQuery)
|
|
7626
|
+
const shellTerminalId = normalizeSessionToken(shellParams.get("terminal_id"))
|
|
7627
|
+
if (shellTerminalId) {
|
|
7628
|
+
if (!runningShellIdsByTerminalId.has(shellTerminalId)) {
|
|
7629
|
+
runningShellIdsByTerminalId.set(shellTerminalId, [])
|
|
7630
|
+
}
|
|
7631
|
+
runningShellIdsByTerminalId.get(shellTerminalId).push(shellIdText)
|
|
7632
|
+
const shellSessionId = normalizeSessionToken(shellParams.get("session"))
|
|
7633
|
+
if (shellSessionId) {
|
|
7634
|
+
runningShellIdByTerminalSession.set(`${shellTerminalId}|${shellSessionId}`, shellIdText)
|
|
7635
|
+
}
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7638
|
+
}
|
|
7332
7639
|
const keys = parseRunningSessionKeys(shellEntry && shellEntry.id ? shellEntry.id : null)
|
|
7333
7640
|
let providerKey = ""
|
|
7334
|
-
let hasForkMarker = false
|
|
7335
7641
|
for (let i = 0; i < keys.length; i++) {
|
|
7336
7642
|
const key = keys[i]
|
|
7337
7643
|
runningSessionKeys.add(key)
|
|
@@ -7342,17 +7648,29 @@ class Server {
|
|
|
7342
7648
|
if (!providerKey) {
|
|
7343
7649
|
providerKey = key.slice(0, separatorIndex)
|
|
7344
7650
|
}
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7651
|
+
}
|
|
7652
|
+
if (!providerKey) {
|
|
7653
|
+
const inferredProvider = inferProviderFromCommand(shellEntry && shellEntry.cmd ? shellEntry.cmd : "")
|
|
7654
|
+
if (inferredProvider) {
|
|
7655
|
+
providerKey = inferredProvider
|
|
7656
|
+
const inferredTokens = parseSessionTokensFromCommand(shellEntry && shellEntry.cmd ? shellEntry.cmd : "")
|
|
7657
|
+
for (let i = 0; i < inferredTokens.length; i++) {
|
|
7658
|
+
const token = inferredTokens[i]
|
|
7659
|
+
const variants = buildSessionTokenVariants(token)
|
|
7660
|
+
for (let j = 0; j < variants.length; j++) {
|
|
7661
|
+
runningSessionKeys.add(`${providerKey}:${variants[j]}`)
|
|
7662
|
+
}
|
|
7349
7663
|
}
|
|
7350
7664
|
}
|
|
7351
7665
|
}
|
|
7352
|
-
if (
|
|
7666
|
+
if (providerKey) {
|
|
7353
7667
|
const cwdKey = normalizeCwdKey(shellEntry && shellEntry.path ? shellEntry.path : "")
|
|
7354
7668
|
if (cwdKey) {
|
|
7355
|
-
|
|
7669
|
+
const contextKey = `${providerKey}|${cwdKey}`
|
|
7670
|
+
runningProviderCwdKeys.add(contextKey)
|
|
7671
|
+
if (/[?&]session=(?:fork(?:[:]|%3A)|[^&]*(?:[:]|%3A)fork(?:[:]|%3A))/i.test(shellIdText)) {
|
|
7672
|
+
runningForkProviderCwdKeys.add(contextKey)
|
|
7673
|
+
}
|
|
7356
7674
|
}
|
|
7357
7675
|
}
|
|
7358
7676
|
}
|
|
@@ -7587,6 +7905,63 @@ class Server {
|
|
|
7587
7905
|
|
|
7588
7906
|
const refreshDiscoveryEntries = async () => {
|
|
7589
7907
|
const byProvider = new Map()
|
|
7908
|
+
const terminalMetadataCache = new Map()
|
|
7909
|
+
const resolveWorkspaceTerminalId = async (providerKey, workingDirectory) => {
|
|
7910
|
+
const normalizedProvider = typeof providerKey === "string" ? providerKey.trim().toLowerCase() : ""
|
|
7911
|
+
const normalizedCwd = typeof workingDirectory === "string" ? workingDirectory.trim() : ""
|
|
7912
|
+
if (!normalizedProvider || !normalizedCwd) {
|
|
7913
|
+
return ""
|
|
7914
|
+
}
|
|
7915
|
+
const resolvedCwd = path.resolve(normalizedCwd)
|
|
7916
|
+
const cacheKey = `${normalizedProvider}|${resolvedCwd}`
|
|
7917
|
+
if (terminalMetadataCache.has(cacheKey)) {
|
|
7918
|
+
return terminalMetadataCache.get(cacheKey)
|
|
7919
|
+
}
|
|
7920
|
+
let terminalId = ""
|
|
7921
|
+
const metadataPath = path.resolve(resolvedCwd, ".pinokio-terminal.json")
|
|
7922
|
+
let parsedMetadata = null
|
|
7923
|
+
try {
|
|
7924
|
+
const rawMetadata = await fs.promises.readFile(metadataPath, "utf8")
|
|
7925
|
+
parsedMetadata = JSON.parse(rawMetadata)
|
|
7926
|
+
if (parsedMetadata && typeof parsedMetadata === "object") {
|
|
7927
|
+
const metadataProvider = typeof parsedMetadata.provider === "string"
|
|
7928
|
+
? parsedMetadata.provider.trim().toLowerCase()
|
|
7929
|
+
: ""
|
|
7930
|
+
const metadataTerminalId = typeof parsedMetadata.terminal_id === "string"
|
|
7931
|
+
? parsedMetadata.terminal_id.trim()
|
|
7932
|
+
: ""
|
|
7933
|
+
if ((!metadataProvider || metadataProvider === normalizedProvider) && metadataTerminalId) {
|
|
7934
|
+
terminalId = metadataTerminalId
|
|
7935
|
+
}
|
|
7936
|
+
}
|
|
7937
|
+
} catch (error) {
|
|
7938
|
+
}
|
|
7939
|
+
if (!terminalId && this.kernel && typeof this.kernel.path === "function") {
|
|
7940
|
+
const providerRoot = path.resolve(this.kernel.path("terminals", normalizedProvider))
|
|
7941
|
+
if (resolvedCwd === providerRoot || resolvedCwd.startsWith(`${providerRoot}${path.sep}`)) {
|
|
7942
|
+
const relative = path.relative(providerRoot, resolvedCwd)
|
|
7943
|
+
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
7944
|
+
const workspaceId = relative.split(path.sep)[0]
|
|
7945
|
+
if (workspaceId) {
|
|
7946
|
+
terminalId = `${normalizedProvider}:${workspaceId}`
|
|
7947
|
+
const nextMetadata = (parsedMetadata && typeof parsedMetadata === "object")
|
|
7948
|
+
? { ...parsedMetadata }
|
|
7949
|
+
: {}
|
|
7950
|
+
if (!nextMetadata.provider) {
|
|
7951
|
+
nextMetadata.provider = normalizedProvider
|
|
7952
|
+
}
|
|
7953
|
+
if (!nextMetadata.cwd) {
|
|
7954
|
+
nextMetadata.cwd = resolvedCwd
|
|
7955
|
+
}
|
|
7956
|
+
nextMetadata.terminal_id = terminalId
|
|
7957
|
+
await fs.promises.writeFile(metadataPath, JSON.stringify(nextMetadata, null, 2), "utf8").catch(() => {})
|
|
7958
|
+
}
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
}
|
|
7962
|
+
terminalMetadataCache.set(cacheKey, terminalId)
|
|
7963
|
+
return terminalId
|
|
7964
|
+
}
|
|
7590
7965
|
for (let i = 0; i < providers.length; i++) {
|
|
7591
7966
|
const provider = providers[i]
|
|
7592
7967
|
for (let j = 0; j < provider.roots.length; j++) {
|
|
@@ -7638,14 +8013,34 @@ class Server {
|
|
|
7638
8013
|
if (!extractedEntry || !extractedEntry.id) {
|
|
7639
8014
|
continue
|
|
7640
8015
|
}
|
|
8016
|
+
const extractedTerminalId = await resolveWorkspaceTerminalId(provider.key, extractedEntry.cwd)
|
|
7641
8017
|
const key = `${provider.key}:${extractedEntry.id}`
|
|
7642
8018
|
const ts = parseSessionTimestamp(extractedEntry.timestamp)
|
|
7643
8019
|
const existing = byProvider.get(key)
|
|
8020
|
+
const existingSourceKind = existing && typeof existing.source_kind === "string" ? existing.source_kind : ""
|
|
8021
|
+
const extractedSourceKind = typeof extractedEntry.source_kind === "string" ? extractedEntry.source_kind : ""
|
|
8022
|
+
const existingResumeCapable = existing && typeof existing.resume_capable === "boolean" ? existing.resume_capable : null
|
|
8023
|
+
const extractedResumeCapable = typeof extractedEntry.resume_capable === "boolean" ? extractedEntry.resume_capable : null
|
|
8024
|
+
const existingResumeDisabledReason = existing && typeof existing.resume_disabled_reason === "string"
|
|
8025
|
+
? existing.resume_disabled_reason
|
|
8026
|
+
: ""
|
|
8027
|
+
const extractedResumeDisabledReason = typeof extractedEntry.resume_disabled_reason === "string"
|
|
8028
|
+
? extractedEntry.resume_disabled_reason
|
|
8029
|
+
: ""
|
|
7644
8030
|
const existingForkCapable = existing && typeof existing.fork_capable === "boolean" ? existing.fork_capable : null
|
|
7645
8031
|
const extractedForkCapable = typeof extractedEntry.fork_capable === "boolean" ? extractedEntry.fork_capable : null
|
|
7646
8032
|
const mergedForkCapable = provider.key === "gemini"
|
|
7647
8033
|
? Boolean(existingForkCapable || extractedForkCapable)
|
|
7648
8034
|
: true
|
|
8035
|
+
let mergedResumeCapable = null
|
|
8036
|
+
if (existingResumeCapable === true || extractedResumeCapable === true) {
|
|
8037
|
+
mergedResumeCapable = true
|
|
8038
|
+
} else if (existingResumeCapable === false || extractedResumeCapable === false) {
|
|
8039
|
+
mergedResumeCapable = false
|
|
8040
|
+
}
|
|
8041
|
+
const mergedResumeDisabledReason = mergedResumeCapable === false
|
|
8042
|
+
? (extractedResumeDisabledReason || existingResumeDisabledReason || "")
|
|
8043
|
+
: ""
|
|
7649
8044
|
const merged = {
|
|
7650
8045
|
...existing,
|
|
7651
8046
|
id: extractedEntry.id,
|
|
@@ -7658,9 +8053,23 @@ class Server {
|
|
|
7658
8053
|
summary: extractedEntry.summary || (existing && existing.summary),
|
|
7659
8054
|
timestamp: existing ? Math.max(existing.timestamp || 0, ts) : ts,
|
|
7660
8055
|
source: existing ? existing.source : file,
|
|
8056
|
+
source_kind: existingSourceKind || extractedSourceKind || null,
|
|
7661
8057
|
metadata: existing ? (existing.metadata || extractedEntry.metadata || null) : (extractedEntry.metadata || null),
|
|
8058
|
+
terminal_id: extractedTerminalId || (existing && typeof existing.terminal_id === "string" ? existing.terminal_id : null),
|
|
7662
8059
|
fork_capable: mergedForkCapable
|
|
7663
8060
|
}
|
|
8061
|
+
if (mergedResumeCapable !== null) {
|
|
8062
|
+
merged.resume_capable = mergedResumeCapable
|
|
8063
|
+
}
|
|
8064
|
+
if (mergedResumeDisabledReason) {
|
|
8065
|
+
merged.resume_disabled_reason = mergedResumeDisabledReason
|
|
8066
|
+
} else if (mergedResumeCapable === true && Object.prototype.hasOwnProperty.call(merged, "resume_disabled_reason")) {
|
|
8067
|
+
delete merged.resume_disabled_reason
|
|
8068
|
+
}
|
|
8069
|
+
if (extractedSourceKind === "gemini_session") {
|
|
8070
|
+
merged.source = file
|
|
8071
|
+
merged.source_kind = extractedSourceKind
|
|
8072
|
+
}
|
|
7664
8073
|
if (!merged.cwd && extractedEntry.cwd) {
|
|
7665
8074
|
merged.cwd = extractedEntry.cwd
|
|
7666
8075
|
}
|
|
@@ -7669,7 +8078,15 @@ class Server {
|
|
|
7669
8078
|
}
|
|
7670
8079
|
if (!existing || ts > existing.timestamp) {
|
|
7671
8080
|
merged.timestamp = ts
|
|
7672
|
-
|
|
8081
|
+
const preserveGeminiSessionSource = provider.key === "gemini"
|
|
8082
|
+
&& existingSourceKind === "gemini_session"
|
|
8083
|
+
&& extractedSourceKind === "gemini_log"
|
|
8084
|
+
if (!preserveGeminiSessionSource) {
|
|
8085
|
+
merged.source = file
|
|
8086
|
+
if (extractedSourceKind) {
|
|
8087
|
+
merged.source_kind = extractedSourceKind
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
7673
8090
|
if (extractedEntry.summary) {
|
|
7674
8091
|
merged.summary = extractedEntry.summary
|
|
7675
8092
|
}
|
|
@@ -7680,7 +8097,14 @@ class Server {
|
|
|
7680
8097
|
}
|
|
7681
8098
|
}
|
|
7682
8099
|
}
|
|
7683
|
-
const entries = Array.from(byProvider.values())
|
|
8100
|
+
const entries = Array.from(byProvider.values())
|
|
8101
|
+
.filter((entry) => {
|
|
8102
|
+
if (!entry || entry.provider !== "gemini") {
|
|
8103
|
+
return true
|
|
8104
|
+
}
|
|
8105
|
+
return entry.source_kind !== "gemini_log"
|
|
8106
|
+
})
|
|
8107
|
+
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
|
|
7684
8108
|
terminalSessionDiscoveryCache.entries = entries
|
|
7685
8109
|
terminalSessionDiscoveryCache.expires = Date.now() + TERMINAL_SESSION_DISCOVERY_CACHE_TTL_MS
|
|
7686
8110
|
return entries
|
|
@@ -7708,7 +8132,7 @@ class Server {
|
|
|
7708
8132
|
const hasCachedEntries = Array.isArray(terminalSessionDiscoveryCache.entries)
|
|
7709
8133
|
const cacheExpired = !hasCachedEntries || terminalSessionDiscoveryCache.expires <= now
|
|
7710
8134
|
let discoveredEntries = hasCachedEntries ? terminalSessionDiscoveryCache.entries : []
|
|
7711
|
-
if (forceDiscovery
|
|
8135
|
+
if (forceDiscovery) {
|
|
7712
8136
|
let refreshed = null
|
|
7713
8137
|
try {
|
|
7714
8138
|
refreshed = await startDiscoveryRefresh(true)
|
|
@@ -7720,6 +8144,24 @@ class Server {
|
|
|
7720
8144
|
} else {
|
|
7721
8145
|
discoveredEntries = Array.isArray(terminalSessionDiscoveryCache.entries) ? terminalSessionDiscoveryCache.entries : []
|
|
7722
8146
|
}
|
|
8147
|
+
} else if (!hasCachedEntries) {
|
|
8148
|
+
const pendingRefresh = startDiscoveryRefresh(false)
|
|
8149
|
+
if (pendingRefresh && typeof pendingRefresh.catch === "function") {
|
|
8150
|
+
pendingRefresh.catch(() => {})
|
|
8151
|
+
}
|
|
8152
|
+
if (!cacheOnly) {
|
|
8153
|
+
let refreshed = null
|
|
8154
|
+
try {
|
|
8155
|
+
refreshed = await startDiscoveryRefresh(true)
|
|
8156
|
+
} catch (error) {
|
|
8157
|
+
refreshed = null
|
|
8158
|
+
}
|
|
8159
|
+
if (Array.isArray(refreshed)) {
|
|
8160
|
+
discoveredEntries = refreshed
|
|
8161
|
+
} else {
|
|
8162
|
+
discoveredEntries = Array.isArray(terminalSessionDiscoveryCache.entries) ? terminalSessionDiscoveryCache.entries : []
|
|
8163
|
+
}
|
|
8164
|
+
}
|
|
7723
8165
|
} else if (cacheExpired) {
|
|
7724
8166
|
const pendingRefresh = startDiscoveryRefresh(false)
|
|
7725
8167
|
if (pendingRefresh && typeof pendingRefresh.catch === "function") {
|
|
@@ -7740,7 +8182,33 @@ class Server {
|
|
|
7740
8182
|
latestDiscoveredByProviderCwd.set(entryKey, entry)
|
|
7741
8183
|
}
|
|
7742
8184
|
}
|
|
7743
|
-
const
|
|
8185
|
+
const getGeminiSourceSessionToken = (sourcePath) => {
|
|
8186
|
+
if (typeof sourcePath !== "string" || sourcePath.trim().length === 0) {
|
|
8187
|
+
return null
|
|
8188
|
+
}
|
|
8189
|
+
const resolved = path.resolve(sourcePath)
|
|
8190
|
+
for (let i = 0; i < geminiCanonicalRoots.length; i++) {
|
|
8191
|
+
const root = geminiCanonicalRoots[i]
|
|
8192
|
+
if (!root) {
|
|
8193
|
+
continue
|
|
8194
|
+
}
|
|
8195
|
+
const resolvedRoot = path.resolve(root)
|
|
8196
|
+
if (!(resolved === resolvedRoot || resolved.startsWith(`${resolvedRoot}${path.sep}`))) {
|
|
8197
|
+
continue
|
|
8198
|
+
}
|
|
8199
|
+
const relative = path.relative(resolvedRoot, resolved)
|
|
8200
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
8201
|
+
continue
|
|
8202
|
+
}
|
|
8203
|
+
const firstSegment = relative.split(path.sep)[0]
|
|
8204
|
+
const normalized = normalizeSessionToken(firstSegment)
|
|
8205
|
+
if (normalized) {
|
|
8206
|
+
return normalized
|
|
8207
|
+
}
|
|
8208
|
+
}
|
|
8209
|
+
return null
|
|
8210
|
+
}
|
|
8211
|
+
const isDiscoveredEntryDirectlyOnline = (entry) => {
|
|
7744
8212
|
if (!entry) {
|
|
7745
8213
|
return false
|
|
7746
8214
|
}
|
|
@@ -7748,12 +8216,44 @@ class Server {
|
|
|
7748
8216
|
return true
|
|
7749
8217
|
}
|
|
7750
8218
|
const providerKey = typeof entry.provider === "string" ? entry.provider.toLowerCase() : ""
|
|
8219
|
+
if (providerKey === "gemini") {
|
|
8220
|
+
const sourceSessionToken = getGeminiSourceSessionToken(entry.source)
|
|
8221
|
+
if (sourceSessionToken && isSessionOnline(providerKey, sourceSessionToken)) {
|
|
8222
|
+
return true
|
|
8223
|
+
}
|
|
8224
|
+
}
|
|
8225
|
+
return false
|
|
8226
|
+
}
|
|
8227
|
+
const directOnlineByProviderCwd = new Set()
|
|
8228
|
+
for (let i = 0; i < discoveredEntries.length; i++) {
|
|
8229
|
+
const entry = discoveredEntries[i]
|
|
8230
|
+
if (!isDiscoveredEntryDirectlyOnline(entry)) {
|
|
8231
|
+
continue
|
|
8232
|
+
}
|
|
8233
|
+
const providerKey = entry && typeof entry.provider === "string" ? entry.provider.toLowerCase() : ""
|
|
8234
|
+
const cwdKey = normalizeCwdKey(entry && entry.cwd ? entry.cwd : "")
|
|
8235
|
+
if (!providerKey || !cwdKey) {
|
|
8236
|
+
continue
|
|
8237
|
+
}
|
|
8238
|
+
directOnlineByProviderCwd.add(`${providerKey}|${cwdKey}`)
|
|
8239
|
+
}
|
|
8240
|
+
const isDiscoveredEntryOnline = (entry) => {
|
|
8241
|
+
if (!entry) {
|
|
8242
|
+
return false
|
|
8243
|
+
}
|
|
8244
|
+
if (isDiscoveredEntryDirectlyOnline(entry)) {
|
|
8245
|
+
return true
|
|
8246
|
+
}
|
|
8247
|
+
const providerKey = typeof entry.provider === "string" ? entry.provider.toLowerCase() : ""
|
|
7751
8248
|
const cwdKey = normalizeCwdKey(entry.cwd || "")
|
|
7752
8249
|
if (!providerKey || !cwdKey) {
|
|
7753
8250
|
return false
|
|
7754
8251
|
}
|
|
7755
8252
|
const contextKey = `${providerKey}|${cwdKey}`
|
|
7756
|
-
if (!
|
|
8253
|
+
if (!runningProviderCwdKeys.has(contextKey)) {
|
|
8254
|
+
return false
|
|
8255
|
+
}
|
|
8256
|
+
if (directOnlineByProviderCwd.has(contextKey) && !runningForkProviderCwdKeys.has(contextKey)) {
|
|
7757
8257
|
return false
|
|
7758
8258
|
}
|
|
7759
8259
|
const latest = latestDiscoveredByProviderCwd.get(contextKey)
|
|
@@ -7764,6 +8264,10 @@ class Server {
|
|
|
7764
8264
|
}
|
|
7765
8265
|
|
|
7766
8266
|
return Array.from(discoveredEntries || [])
|
|
8267
|
+
.filter((entry) => {
|
|
8268
|
+
const workingDirectory = entry && typeof entry.cwd === "string" ? entry.cwd.trim() : ""
|
|
8269
|
+
return workingDirectory.length > 0
|
|
8270
|
+
})
|
|
7767
8271
|
.sort((a, b) => {
|
|
7768
8272
|
const aOnline = isDiscoveredEntryOnline(a) ? 1 : 0
|
|
7769
8273
|
const bOnline = isDiscoveredEntryOnline(b) ? 1 : 0
|
|
@@ -7775,7 +8279,10 @@ class Server {
|
|
|
7775
8279
|
return tb - ta
|
|
7776
8280
|
})
|
|
7777
8281
|
.map((entry, index) => {
|
|
7778
|
-
const workingDirectory = entry.cwd
|
|
8282
|
+
const workingDirectory = typeof entry.cwd === "string" ? entry.cwd.trim() : ""
|
|
8283
|
+
const terminalId = typeof entry.terminal_id === "string" ? entry.terminal_id.trim() : ""
|
|
8284
|
+
const entryResumeCapable = typeof entry.resume_capable === "boolean" ? entry.resume_capable : null
|
|
8285
|
+
const resumeCapable = workingDirectory.length > 0 && entryResumeCapable !== false
|
|
7779
8286
|
const codexResumeCommand = buildCodexResumeBaseCommand(entry)
|
|
7780
8287
|
const resumeBaseCommand = codexResumeCommand.command
|
|
7781
8288
|
const buildTemplatedSessionCommand = (template, sessionId) => {
|
|
@@ -7798,45 +8305,37 @@ class Server {
|
|
|
7798
8305
|
const resumeCommand = buildTemplatedSessionCommand(resumeTemplate, entry.id)
|
|
7799
8306
|
let forkCommand = buildTemplatedSessionCommand(forkTemplate, entry.id)
|
|
7800
8307
|
let forkCapable = forkCommand !== resumeCommand
|
|
8308
|
+
let resumeDisabledReason = ""
|
|
7801
8309
|
let forkDisabledReason = ""
|
|
8310
|
+
if (!resumeCapable) {
|
|
8311
|
+
const explicitResumeReason = typeof entry.resume_disabled_reason === "string"
|
|
8312
|
+
? entry.resume_disabled_reason.trim()
|
|
8313
|
+
: ""
|
|
8314
|
+
if (explicitResumeReason) {
|
|
8315
|
+
resumeDisabledReason = explicitResumeReason
|
|
8316
|
+
} else if (workingDirectory.length === 0) {
|
|
8317
|
+
resumeDisabledReason = `${entry.providerLabel || "Session"} resume unavailable: missing working directory metadata.`
|
|
8318
|
+
} else {
|
|
8319
|
+
resumeDisabledReason = `${entry.providerLabel || "Session"} resume unavailable for this session.`
|
|
8320
|
+
}
|
|
8321
|
+
}
|
|
7802
8322
|
if (entry.provider === "gemini" && typeof entry.source === "string" && entry.source.length > 0) {
|
|
7803
8323
|
forkCapable = Boolean(entry.fork_capable)
|
|
7804
8324
|
if (forkCapable) {
|
|
7805
|
-
const
|
|
7806
|
-
|
|
7807
|
-
'const path = require("path")',
|
|
7808
|
-
'const crypto = require("crypto")',
|
|
7809
|
-
'const sourcePath = process.argv[1]',
|
|
7810
|
-
'const expectedSessionId = process.argv[2]',
|
|
7811
|
-
'if (!sourcePath) process.exit(1)',
|
|
7812
|
-
'const raw = fs.readFileSync(sourcePath, "utf8")',
|
|
7813
|
-
'const payload = JSON.parse(raw)',
|
|
7814
|
-
'if (!payload || typeof payload !== "object") process.exit(1)',
|
|
7815
|
-
'const sourceSessionId = typeof payload.sessionId === "string" ? payload.sessionId : (typeof payload.session_id === "string" ? payload.session_id : "")',
|
|
7816
|
-
'if (!sourceSessionId) process.exit(1)',
|
|
7817
|
-
'if (expectedSessionId && sourceSessionId !== expectedSessionId) process.exit(1)',
|
|
7818
|
-
'if (!Array.isArray(payload.messages)) process.exit(1)',
|
|
7819
|
-
'const newSessionId = typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`',
|
|
7820
|
-
'const nowIso = new Date().toISOString()',
|
|
7821
|
-
'payload.sessionId = newSessionId',
|
|
7822
|
-
'if (typeof payload.session_id === "string") payload.session_id = newSessionId',
|
|
7823
|
-
'if (typeof payload.startTime === "string") payload.startTime = nowIso',
|
|
7824
|
-
'if (typeof payload.lastUpdated === "string") payload.lastUpdated = nowIso',
|
|
7825
|
-
'const stamp = nowIso.replace(/[:.]/g, "-")',
|
|
7826
|
-
'const filename = `session-${stamp}-${newSessionId.slice(0, 8)}.json`',
|
|
7827
|
-
'const targetPath = path.join(path.dirname(sourcePath), filename)',
|
|
7828
|
-
'fs.writeFileSync(targetPath, JSON.stringify(payload, null, 2), "utf8")',
|
|
7829
|
-
'process.stdout.write(newSessionId)'
|
|
7830
|
-
].join(";")
|
|
7831
|
-
const cloneAndResume = `FORK_SESSION_ID=$(node -e ${JSON.stringify(cloneScript)} ${JSON.stringify(entry.source)} ${JSON.stringify(entry.id)}) && ${resumeBaseCommand} --resume "$FORK_SESSION_ID"`
|
|
8325
|
+
const geminiForkResumeScript = path.resolve(__dirname, "scripts", "gemini_fork_and_resume.js")
|
|
8326
|
+
const cloneAndResume = `node ${JSON.stringify(geminiForkResumeScript)} ${JSON.stringify(entry.source)} ${JSON.stringify(entry.id)}`
|
|
7832
8327
|
forkCommand = cloneAndResume
|
|
7833
8328
|
} else {
|
|
7834
8329
|
forkCommand = ""
|
|
7835
8330
|
forkDisabledReason = "Gemini fork unavailable for this session format. Use /chat save and /chat resume to branch manually."
|
|
7836
8331
|
}
|
|
7837
8332
|
}
|
|
7838
|
-
if (
|
|
7839
|
-
|
|
8333
|
+
if (!resumeCapable) {
|
|
8334
|
+
forkCapable = false
|
|
8335
|
+
forkCommand = ""
|
|
8336
|
+
if (!forkDisabledReason) {
|
|
8337
|
+
forkDisabledReason = resumeDisabledReason
|
|
8338
|
+
}
|
|
7840
8339
|
}
|
|
7841
8340
|
if (!forkCapable && !forkDisabledReason) {
|
|
7842
8341
|
forkDisabledReason = `${entry.providerLabel || "Session"} fork unavailable for this session.`
|
|
@@ -7849,6 +8348,9 @@ class Server {
|
|
|
7849
8348
|
params.set("path", workingDirectory)
|
|
7850
8349
|
params.set("cwd", workingDirectory)
|
|
7851
8350
|
params.set("session", sessionValue)
|
|
8351
|
+
if (terminalId) {
|
|
8352
|
+
params.set("terminal_id", terminalId)
|
|
8353
|
+
}
|
|
7852
8354
|
params.set("message", command)
|
|
7853
8355
|
params.set("input", "1")
|
|
7854
8356
|
if (forkMode) {
|
|
@@ -7856,11 +8358,23 @@ class Server {
|
|
|
7856
8358
|
}
|
|
7857
8359
|
return `${route}?${params.toString()}`
|
|
7858
8360
|
}
|
|
7859
|
-
|
|
7860
|
-
const forkUrl = forkCapable && forkCommand
|
|
8361
|
+
let resumeUrl = resumeCapable ? buildShellRoute(resumeCommand, entry.id, false) : "#"
|
|
8362
|
+
const forkUrl = resumeCapable && forkCapable && forkCommand
|
|
7861
8363
|
? buildShellRoute(forkCommand, `fork:${entry.id}`, true)
|
|
7862
8364
|
: ""
|
|
7863
8365
|
const online = isDiscoveredEntryOnline(entry)
|
|
8366
|
+
const normalizedTerminalId = normalizeSessionToken(terminalId) || terminalId
|
|
8367
|
+
if (resumeCapable && normalizedTerminalId) {
|
|
8368
|
+
const normalizedEntrySessionId = normalizeSessionToken(entry.id)
|
|
8369
|
+
const exactShellId = normalizedEntrySessionId
|
|
8370
|
+
? runningShellIdByTerminalSession.get(`${normalizedTerminalId}|${normalizedEntrySessionId}`)
|
|
8371
|
+
: null
|
|
8372
|
+
const terminalShellIds = runningShellIdsByTerminalId.get(normalizedTerminalId) || []
|
|
8373
|
+
const resolvedShellId = exactShellId || terminalShellIds[terminalShellIds.length - 1] || ""
|
|
8374
|
+
if (resolvedShellId) {
|
|
8375
|
+
resumeUrl = resolvedShellId.startsWith("/") ? resolvedShellId : `/${resolvedShellId}`
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
7864
8378
|
return {
|
|
7865
8379
|
name: title,
|
|
7866
8380
|
description: `${entry.providerLabel} · ${entry.cwd || "cwd unavailable"}`,
|
|
@@ -7869,19 +8383,52 @@ class Server {
|
|
|
7869
8383
|
index,
|
|
7870
8384
|
url: resumeUrl,
|
|
7871
8385
|
browser_url: resumeUrl,
|
|
8386
|
+
resume_capable: resumeCapable,
|
|
8387
|
+
resume_disabled_reason: resumeDisabledReason || null,
|
|
7872
8388
|
fork_url: forkUrl,
|
|
7873
8389
|
fork_capable: forkCapable,
|
|
7874
8390
|
fork_disabled_reason: forkDisabledReason || null,
|
|
7875
8391
|
filepath: entry.cwd || "",
|
|
7876
8392
|
provider_label: entry.providerLabel || entry.provider || "Session",
|
|
7877
8393
|
cwd: entry.cwd || "",
|
|
7878
|
-
timestamp: entry.timestamp || null
|
|
8394
|
+
timestamp: entry.timestamp || null,
|
|
8395
|
+
terminal_id: terminalId || null
|
|
8396
|
+
}
|
|
8397
|
+
})
|
|
8398
|
+
}
|
|
8399
|
+
const syncTerminalSessionRegistry = async () => {
|
|
8400
|
+
if (!terminalSessionRegistrySyncPromise) {
|
|
8401
|
+
terminalSessionRegistrySyncPromise = (async () => {
|
|
8402
|
+
const discoveredItems = await buildTerminalSessions(true)
|
|
8403
|
+
const normalizedDiscoveredItems = coerceTerminalRegistryItems(discoveredItems)
|
|
8404
|
+
const existingRegistry = await readTerminalSessionRegistry()
|
|
8405
|
+
const hasChanged = !terminalRegistryItemsEqual(existingRegistry.items, normalizedDiscoveredItems)
|
|
8406
|
+
if (hasChanged || !existingRegistry.exists) {
|
|
8407
|
+
await writeTerminalSessionRegistry(normalizedDiscoveredItems)
|
|
8408
|
+
}
|
|
8409
|
+
return {
|
|
8410
|
+
items: hasChanged ? normalizedDiscoveredItems : existingRegistry.items,
|
|
8411
|
+
changed: hasChanged
|
|
7879
8412
|
}
|
|
8413
|
+
})().finally(() => {
|
|
8414
|
+
terminalSessionRegistrySyncPromise = null
|
|
7880
8415
|
})
|
|
8416
|
+
}
|
|
8417
|
+
return terminalSessionRegistrySyncPromise
|
|
7881
8418
|
}
|
|
7882
8419
|
|
|
7883
8420
|
if (req.query.mode === "terminals" && (req.query.fetch === "1" || req.query.format === "json")) {
|
|
7884
8421
|
const includeSkills = req.query.skills === "1"
|
|
8422
|
+
const hasLimitParam = Object.prototype.hasOwnProperty.call(req.query, "limit")
|
|
8423
|
+
const parsedLimit = Number.parseInt(req.query.limit, 10)
|
|
8424
|
+
const pageLimit = hasLimitParam && Number.isFinite(parsedLimit)
|
|
8425
|
+
? Math.min(Math.max(parsedLimit, 1), 500)
|
|
8426
|
+
: null
|
|
8427
|
+
const parsedCursor = Number.parseInt(req.query.cursor, 10)
|
|
8428
|
+
const pageCursor = pageLimit !== null && Number.isFinite(parsedCursor) && parsedCursor > 0
|
|
8429
|
+
? parsedCursor
|
|
8430
|
+
: 0
|
|
8431
|
+
const searchQuery = typeof req.query.q === "string" ? req.query.q.trim().toLowerCase() : ""
|
|
7885
8432
|
let serializedSkills = null
|
|
7886
8433
|
if (includeSkills) {
|
|
7887
8434
|
const terminalSkills = await listTerminalSkills()
|
|
@@ -7908,15 +8455,60 @@ class Server {
|
|
|
7908
8455
|
res.status(400).json(errorPayload)
|
|
7909
8456
|
return
|
|
7910
8457
|
}
|
|
7911
|
-
|
|
7912
|
-
const
|
|
8458
|
+
await scrubTerminalSessionRegistryOnlineStateAtBoot().catch(() => {})
|
|
8459
|
+
const syncRequested = req.query.sync === "1"
|
|
8460
|
+
|| req.query.refresh === "1"
|
|
8461
|
+
|| req.query.fresh === "1"
|
|
8462
|
+
|| req.query.force === "1"
|
|
8463
|
+
let terminalItems = []
|
|
8464
|
+
if (syncRequested) {
|
|
8465
|
+
const syncResult = await syncTerminalSessionRegistry()
|
|
8466
|
+
terminalItems = Array.isArray(syncResult && syncResult.items) ? syncResult.items : []
|
|
8467
|
+
} else {
|
|
8468
|
+
const registry = await readTerminalSessionRegistry()
|
|
8469
|
+
terminalItems = Array.isArray(registry.items) ? registry.items : []
|
|
8470
|
+
if (terminalItems.length === 0) {
|
|
8471
|
+
const syncResult = await syncTerminalSessionRegistry()
|
|
8472
|
+
terminalItems = Array.isArray(syncResult && syncResult.items) ? syncResult.items : []
|
|
8473
|
+
}
|
|
8474
|
+
}
|
|
7913
8475
|
for (let i = 0; i < terminalItems.length; i++) {
|
|
7914
8476
|
terminalItems[i].index = i
|
|
7915
8477
|
}
|
|
8478
|
+
let filteredItems = terminalItems
|
|
8479
|
+
if (searchQuery) {
|
|
8480
|
+
filteredItems = terminalItems.filter((item) => {
|
|
8481
|
+
const haystack = [
|
|
8482
|
+
item && item.name ? item.name : "",
|
|
8483
|
+
item && item.description ? item.description : "",
|
|
8484
|
+
item && item.provider_label ? item.provider_label : "",
|
|
8485
|
+
item && item.cwd ? item.cwd : "",
|
|
8486
|
+
item && item.uri ? item.uri : ""
|
|
8487
|
+
].join(" ").toLowerCase()
|
|
8488
|
+
return haystack.includes(searchQuery)
|
|
8489
|
+
})
|
|
8490
|
+
}
|
|
8491
|
+
let responseItems = filteredItems
|
|
8492
|
+
let pagination = null
|
|
8493
|
+
if (pageLimit !== null) {
|
|
8494
|
+
const safeCursor = Math.max(0, Math.min(pageCursor, filteredItems.length))
|
|
8495
|
+
const sliceEnd = Math.min(filteredItems.length, safeCursor + pageLimit)
|
|
8496
|
+
responseItems = filteredItems.slice(safeCursor, sliceEnd)
|
|
8497
|
+
pagination = {
|
|
8498
|
+
cursor: safeCursor,
|
|
8499
|
+
limit: pageLimit,
|
|
8500
|
+
total: filteredItems.length,
|
|
8501
|
+
hasMore: sliceEnd < filteredItems.length,
|
|
8502
|
+
nextCursor: sliceEnd < filteredItems.length ? sliceEnd : null
|
|
8503
|
+
}
|
|
8504
|
+
}
|
|
7916
8505
|
const responsePayload = {
|
|
7917
|
-
items:
|
|
8506
|
+
items: responseItems,
|
|
7918
8507
|
providers: getTerminalStarterProviders().map((provider) => ({ key: provider.key, label: provider.label }))
|
|
7919
8508
|
}
|
|
8509
|
+
if (pagination) {
|
|
8510
|
+
responsePayload.pagination = pagination
|
|
8511
|
+
}
|
|
7920
8512
|
if (includeSkills) {
|
|
7921
8513
|
responsePayload.skills = Array.isArray(serializedSkills) ? serializedSkills : []
|
|
7922
8514
|
}
|
|
@@ -7929,28 +8521,13 @@ class Server {
|
|
|
7929
8521
|
res.redirect("/home?mode=settings")
|
|
7930
8522
|
return
|
|
7931
8523
|
}
|
|
7932
|
-
const initialTerminalItems = await buildTerminalSessions()
|
|
7933
|
-
let terminalItems = Array.isArray(initialTerminalItems) ? initialTerminalItems : []
|
|
7934
|
-
// Avoid serving a stale empty-first paint: if cached result is empty,
|
|
7935
|
-
// force one fresh discovery pass before rendering.
|
|
7936
|
-
if (terminalItems.length === 0) {
|
|
7937
|
-
const freshTerminalItems = await buildTerminalSessions(true)
|
|
7938
|
-
if (Array.isArray(freshTerminalItems) && freshTerminalItems.length > 0) {
|
|
7939
|
-
terminalItems = freshTerminalItems
|
|
7940
|
-
}
|
|
7941
|
-
}
|
|
7942
|
-
let listItems = terminalItems
|
|
7943
|
-
for (let i = 0; i < listItems.length; i++) {
|
|
7944
|
-
listItems[i].index = i
|
|
7945
|
-
}
|
|
7946
|
-
|
|
7947
8524
|
res.render("terminals", {
|
|
7948
8525
|
logo: this.logo,
|
|
7949
8526
|
theme: this.theme,
|
|
7950
8527
|
agent: req.agent,
|
|
7951
8528
|
query: req.query,
|
|
7952
8529
|
uri: "/home?mode=terminals",
|
|
7953
|
-
items:
|
|
8530
|
+
items: [],
|
|
7954
8531
|
providers: getTerminalStarterProviders().map((provider) => ({ key: provider.key, label: provider.label })),
|
|
7955
8532
|
skills: [],
|
|
7956
8533
|
ishome: false
|
|
@@ -8220,6 +8797,160 @@ class Server {
|
|
|
8220
8797
|
|
|
8221
8798
|
this.app.get("/home", renderHomePage)
|
|
8222
8799
|
|
|
8800
|
+
const normalizePathForComparison = (value) => {
|
|
8801
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
8802
|
+
return ""
|
|
8803
|
+
}
|
|
8804
|
+
let resolved = path.resolve(value.trim())
|
|
8805
|
+
if (process.platform === "win32") {
|
|
8806
|
+
resolved = resolved.toLowerCase()
|
|
8807
|
+
}
|
|
8808
|
+
return resolved
|
|
8809
|
+
}
|
|
8810
|
+
const isPathWithin = (candidate, parent) => {
|
|
8811
|
+
const normalizedCandidate = normalizePathForComparison(candidate)
|
|
8812
|
+
const normalizedParent = normalizePathForComparison(parent)
|
|
8813
|
+
if (!normalizedCandidate || !normalizedParent) {
|
|
8814
|
+
return false
|
|
8815
|
+
}
|
|
8816
|
+
if (normalizedCandidate === normalizedParent) {
|
|
8817
|
+
return true
|
|
8818
|
+
}
|
|
8819
|
+
const withSep = normalizedParent.endsWith(path.sep) ? normalizedParent : `${normalizedParent}${path.sep}`
|
|
8820
|
+
return normalizedCandidate.startsWith(withSep)
|
|
8821
|
+
}
|
|
8822
|
+
|
|
8823
|
+
this.app.post("/terminals/deploy/local", ex(async (req, res) => {
|
|
8824
|
+
const body = req.body && typeof req.body === "object" ? req.body : {}
|
|
8825
|
+
const folderName = typeof body.folderName === "string" ? body.folderName.trim() : ""
|
|
8826
|
+
const sessionUri = typeof body.sessionUri === "string" ? body.sessionUri.trim().toLowerCase() : ""
|
|
8827
|
+
const sessionCwdHint = typeof body.sessionCwd === "string" ? body.sessionCwd.trim() : ""
|
|
8828
|
+
|
|
8829
|
+
if (!folderName) {
|
|
8830
|
+
res.status(400).json({
|
|
8831
|
+
ok: false,
|
|
8832
|
+
error: "folderName is required"
|
|
8833
|
+
})
|
|
8834
|
+
return
|
|
8835
|
+
}
|
|
8836
|
+
if (folderName === "." || folderName === ".." || /[\\/]/.test(folderName) || folderName.includes("\0")) {
|
|
8837
|
+
res.status(400).json({
|
|
8838
|
+
ok: false,
|
|
8839
|
+
error: "Invalid folder name"
|
|
8840
|
+
})
|
|
8841
|
+
return
|
|
8842
|
+
}
|
|
8843
|
+
|
|
8844
|
+
const registryPath = this.kernel.path("cache", "terminals", "sessions.json")
|
|
8845
|
+
let registryItems = []
|
|
8846
|
+
try {
|
|
8847
|
+
const rawRegistry = await fs.promises.readFile(registryPath, "utf8")
|
|
8848
|
+
const parsedRegistry = JSON.parse(rawRegistry)
|
|
8849
|
+
if (parsedRegistry && Array.isArray(parsedRegistry.items)) {
|
|
8850
|
+
registryItems = parsedRegistry.items
|
|
8851
|
+
}
|
|
8852
|
+
} catch (error) {
|
|
8853
|
+
}
|
|
8854
|
+
|
|
8855
|
+
let sourcePath = ""
|
|
8856
|
+
if (sessionUri) {
|
|
8857
|
+
for (let i = 0; i < registryItems.length; i++) {
|
|
8858
|
+
const entry = registryItems[i]
|
|
8859
|
+
const entryUri = entry && typeof entry.uri === "string" ? entry.uri.trim().toLowerCase() : ""
|
|
8860
|
+
const entryCwd = entry && typeof entry.cwd === "string" ? entry.cwd.trim() : ""
|
|
8861
|
+
if (entryUri && entryCwd && entryUri === sessionUri) {
|
|
8862
|
+
sourcePath = entryCwd
|
|
8863
|
+
break
|
|
8864
|
+
}
|
|
8865
|
+
}
|
|
8866
|
+
}
|
|
8867
|
+
|
|
8868
|
+
if (!sourcePath && sessionCwdHint) {
|
|
8869
|
+
const normalizedHint = normalizePathForComparison(sessionCwdHint)
|
|
8870
|
+
for (let i = 0; i < registryItems.length; i++) {
|
|
8871
|
+
const entry = registryItems[i]
|
|
8872
|
+
const entryCwd = entry && typeof entry.cwd === "string" ? entry.cwd.trim() : ""
|
|
8873
|
+
if (entryCwd && normalizePathForComparison(entryCwd) === normalizedHint) {
|
|
8874
|
+
sourcePath = entryCwd
|
|
8875
|
+
break
|
|
8876
|
+
}
|
|
8877
|
+
}
|
|
8878
|
+
if (!sourcePath) {
|
|
8879
|
+
const terminalsRoot = this.kernel.path("terminals")
|
|
8880
|
+
if (isPathWithin(normalizedHint, terminalsRoot)) {
|
|
8881
|
+
sourcePath = normalizedHint
|
|
8882
|
+
}
|
|
8883
|
+
}
|
|
8884
|
+
}
|
|
8885
|
+
|
|
8886
|
+
if (!sourcePath) {
|
|
8887
|
+
res.status(400).json({
|
|
8888
|
+
ok: false,
|
|
8889
|
+
error: "Current session folder not found."
|
|
8890
|
+
})
|
|
8891
|
+
return
|
|
8892
|
+
}
|
|
8893
|
+
|
|
8894
|
+
let sourceStats = null
|
|
8895
|
+
try {
|
|
8896
|
+
sourceStats = await fs.promises.stat(sourcePath)
|
|
8897
|
+
} catch (error) {
|
|
8898
|
+
}
|
|
8899
|
+
if (!sourceStats || !sourceStats.isDirectory()) {
|
|
8900
|
+
res.status(400).json({
|
|
8901
|
+
ok: false,
|
|
8902
|
+
error: "Current session folder not found."
|
|
8903
|
+
})
|
|
8904
|
+
return
|
|
8905
|
+
}
|
|
8906
|
+
|
|
8907
|
+
const apiRoot = this.kernel.path("api")
|
|
8908
|
+
await fs.promises.mkdir(apiRoot, { recursive: true })
|
|
8909
|
+
const targetPath = this.kernel.path("api", folderName)
|
|
8910
|
+
const normalizedSourcePath = normalizePathForComparison(sourcePath)
|
|
8911
|
+
const normalizedTargetPath = normalizePathForComparison(targetPath)
|
|
8912
|
+
if (!normalizedTargetPath || !isPathWithin(normalizedTargetPath, apiRoot)) {
|
|
8913
|
+
res.status(400).json({
|
|
8914
|
+
ok: false,
|
|
8915
|
+
error: "Invalid destination path."
|
|
8916
|
+
})
|
|
8917
|
+
return
|
|
8918
|
+
}
|
|
8919
|
+
if (isPathWithin(normalizedTargetPath, normalizedSourcePath)) {
|
|
8920
|
+
res.status(400).json({
|
|
8921
|
+
ok: false,
|
|
8922
|
+
error: "Invalid deployment target."
|
|
8923
|
+
})
|
|
8924
|
+
return
|
|
8925
|
+
}
|
|
8926
|
+
|
|
8927
|
+
const targetExists = await fs.promises.access(targetPath, fs.constants.F_OK).then(() => true).catch(() => false)
|
|
8928
|
+
if (targetExists) {
|
|
8929
|
+
res.json({
|
|
8930
|
+
ok: false,
|
|
8931
|
+
code: "exists",
|
|
8932
|
+
error: "Folder already exists"
|
|
8933
|
+
})
|
|
8934
|
+
return
|
|
8935
|
+
}
|
|
8936
|
+
|
|
8937
|
+
try {
|
|
8938
|
+
await fs.promises.cp(sourcePath, targetPath, { recursive: true })
|
|
8939
|
+
} catch (error) {
|
|
8940
|
+
res.status(500).json({
|
|
8941
|
+
ok: false,
|
|
8942
|
+
error: error && error.message ? error.message : "Failed to copy folder."
|
|
8943
|
+
})
|
|
8944
|
+
return
|
|
8945
|
+
}
|
|
8946
|
+
|
|
8947
|
+
res.json({
|
|
8948
|
+
ok: true,
|
|
8949
|
+
folder: folderName,
|
|
8950
|
+
path: targetPath
|
|
8951
|
+
})
|
|
8952
|
+
}))
|
|
8953
|
+
|
|
8223
8954
|
this.app.post("/terminals/start", ex(async (req, res) => {
|
|
8224
8955
|
const providers = getTerminalStarterProviders()
|
|
8225
8956
|
const providerMap = new Map(providers.map((provider) => [provider.key, provider]))
|
|
@@ -8237,6 +8968,7 @@ class Server {
|
|
|
8237
8968
|
const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
|
8238
8969
|
const shortId = Math.random().toString(36).slice(2, 8)
|
|
8239
8970
|
const folderName = `${timestamp}-${shortId}`
|
|
8971
|
+
const terminalId = `${provider.key}:${folderName}`
|
|
8240
8972
|
const terminalsRoot = this.kernel.path("terminals")
|
|
8241
8973
|
const providerRoot = path.resolve(terminalsRoot, provider.key)
|
|
8242
8974
|
const sessionCwd = path.resolve(providerRoot, folderName)
|
|
@@ -8288,6 +9020,18 @@ class Server {
|
|
|
8288
9020
|
}
|
|
8289
9021
|
|
|
8290
9022
|
await fs.promises.mkdir(sessionCwd, { recursive: true })
|
|
9023
|
+
try {
|
|
9024
|
+
await this.kernel.exec({
|
|
9025
|
+
message: ["git init"],
|
|
9026
|
+
path: sessionCwd
|
|
9027
|
+
}, () => {})
|
|
9028
|
+
} catch (error) {
|
|
9029
|
+
await fs.promises.rm(sessionCwd, { recursive: true, force: true }).catch(() => {})
|
|
9030
|
+
res.status(500).json({
|
|
9031
|
+
error: error && error.message ? error.message : "Failed to initialize git repository for the new session."
|
|
9032
|
+
})
|
|
9033
|
+
return
|
|
9034
|
+
}
|
|
8291
9035
|
const copiedUploads = []
|
|
8292
9036
|
if (requestedUploadToken) {
|
|
8293
9037
|
const uploadDir = path.resolve(this.kernel.path("tmp", "create", requestedUploadToken))
|
|
@@ -8331,6 +9075,7 @@ class Server {
|
|
|
8331
9075
|
await fs.promises.writeFile(metadataPath, JSON.stringify({
|
|
8332
9076
|
provider: provider.key,
|
|
8333
9077
|
label: provider.label,
|
|
9078
|
+
terminal_id: terminalId,
|
|
8334
9079
|
created_at: now.toISOString(),
|
|
8335
9080
|
cwd: sessionCwd,
|
|
8336
9081
|
command: provider.startCommand || provider.command,
|
|
@@ -8342,6 +9087,7 @@ class Server {
|
|
|
8342
9087
|
const params = new URLSearchParams()
|
|
8343
9088
|
params.set("path", sessionCwd)
|
|
8344
9089
|
params.set("cwd", sessionCwd)
|
|
9090
|
+
params.set("terminal_id", terminalId)
|
|
8345
9091
|
params.set("message", provider.startCommand || provider.command)
|
|
8346
9092
|
params.set("input", "1")
|
|
8347
9093
|
const route = `/shell/start-${provider.key}-${folderName}`
|
|
@@ -9131,12 +9877,37 @@ class Server {
|
|
|
9131
9877
|
|
|
9132
9878
|
let baseShellId = "shell/" + decodeURIComponent(req.params.id)
|
|
9133
9879
|
const sessionId = typeof req.query.session === "string" && req.query.session.length > 0 ? req.query.session : null
|
|
9880
|
+
const terminalId = typeof req.query.terminal_id === "string" && req.query.terminal_id.length > 0 ? req.query.terminal_id : null
|
|
9881
|
+
const isForkRequest = req.query.fork === "1"
|
|
9134
9882
|
let id = baseShellId
|
|
9135
|
-
if (sessionId) {
|
|
9136
|
-
|
|
9883
|
+
if (sessionId || terminalId) {
|
|
9884
|
+
const idParams = new URLSearchParams()
|
|
9885
|
+
if (sessionId) {
|
|
9886
|
+
idParams.set("session", sessionId)
|
|
9887
|
+
}
|
|
9888
|
+
if (terminalId) {
|
|
9889
|
+
idParams.set("terminal_id", terminalId)
|
|
9890
|
+
}
|
|
9891
|
+
id = `${baseShellId}?${idParams.toString()}`
|
|
9892
|
+
}
|
|
9893
|
+
let shell = this.kernel.shell.get(id)
|
|
9894
|
+
if (shell && terminalId && !isForkRequest) {
|
|
9895
|
+
req.query.message = ""
|
|
9137
9896
|
}
|
|
9138
9897
|
let target = req.query.target ? req.query.target : null
|
|
9139
|
-
|
|
9898
|
+
const rawPathParam = typeof req.query.path === "string" && req.query.path.length > 0
|
|
9899
|
+
? decodeURIComponent(req.query.path)
|
|
9900
|
+
: ""
|
|
9901
|
+
const rawCwdParam = typeof req.query.cwd === "string" && req.query.cwd.length > 0
|
|
9902
|
+
? decodeURIComponent(req.query.cwd)
|
|
9903
|
+
: ""
|
|
9904
|
+
const shellPath = shell && typeof shell.path === "string" && shell.path.length > 0
|
|
9905
|
+
? shell.path
|
|
9906
|
+
: ""
|
|
9907
|
+
const resolvedPath = rawPathParam || rawCwdParam || shellPath
|
|
9908
|
+
let cwd = resolvedPath
|
|
9909
|
+
? this.kernel.path(this.kernel.api.filePath(resolvedPath))
|
|
9910
|
+
: this.kernel.homedir
|
|
9140
9911
|
let message = req.query.message ? decodeURIComponent(req.query.message) : null
|
|
9141
9912
|
//let message = req.query.message ? req.query.message : null
|
|
9142
9913
|
let venv = req.query.venv ? decodeURIComponent(req.query.venv) : null
|
|
@@ -9173,8 +9944,7 @@ class Server {
|
|
|
9173
9944
|
// pattern[key] = req.query[pattern_key]
|
|
9174
9945
|
// }
|
|
9175
9946
|
// }
|
|
9176
|
-
|
|
9177
|
-
let shell = this.kernel.shell.get(id)
|
|
9947
|
+
await ensureCodexSelectedSkillFrontmatter(cwd).catch(() => {})
|
|
9178
9948
|
res.render("shell", {
|
|
9179
9949
|
target,
|
|
9180
9950
|
filepath: cwd,
|