plotlink-ows 1.0.20 → 1.0.23

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.
@@ -67,6 +67,9 @@ settings.post("/generate-binding", async (c) => {
67
67
  agentName: (config.agentName as string) || undefined,
68
68
  agentDescription: (config.agentDescription as string) || undefined,
69
69
  agentGenre: (config.agentGenre as string) || undefined,
70
+ agentLlmModel: (config.agentLlmModel as string) || undefined,
71
+ agentRegisteredBy: (config.agentRegisteredBy as string) || undefined,
72
+ agentRegisteredAt: (config.agentRegisteredAt as string) || undefined,
70
73
  });
71
74
  } catch (err: unknown) {
72
75
  const msg = err instanceof Error ? err.message : "Failed to generate binding proof";
@@ -107,13 +110,14 @@ settings.post("/register-agent", async (c) => {
107
110
  } catch { /* not registered — continue */ }
108
111
 
109
112
  // Build agentURI as inline JSON
113
+ const registeredAt = new Date().toISOString();
110
114
  const agentURI = JSON.stringify({
111
115
  name: body.name.trim(),
112
116
  description: body.description.trim(),
113
117
  ...(body.genre?.trim() && { genre: body.genre.trim() }),
114
118
  llmModel: "Claude",
115
119
  registeredBy: "plotlink-ows",
116
- registeredAt: new Date().toISOString(),
120
+ registeredAt,
117
121
  });
118
122
 
119
123
  // Create OWS-backed wallet client and call register()
@@ -155,12 +159,15 @@ settings.post("/register-agent", async (c) => {
155
159
  return c.json({ error: "Transaction succeeded but Registered event not found" }, 500);
156
160
  }
157
161
 
158
- // Cache agent data in config.json (survives npx reinstalls, no Prisma dependency)
162
+ // Cache full tokenURI data in config.json (survives npx reinstalls, no Prisma dependency)
159
163
  writeConfig({
160
164
  agentId,
161
- agentName: body.name,
162
- agentDescription: body.description,
163
- ...(body.genre && { agentGenre: body.genre }),
165
+ agentName: body.name.trim(),
166
+ agentDescription: body.description.trim(),
167
+ ...(body.genre?.trim() && { agentGenre: body.genre.trim() }),
168
+ agentLlmModel: "Claude",
169
+ agentRegisteredBy: "plotlink-ows",
170
+ agentRegisteredAt: registeredAt,
164
171
  });
165
172
 
166
173
  return c.json({
@@ -84,14 +84,19 @@ function spawnPty(storyName: string, opts?: { sessionId?: string; resume?: boole
84
84
  ptySessions.set(storyName, session);
85
85
 
86
86
  term.onExit(({ exitCode }) => {
87
- const s = ptySessions.get(storyName);
88
- if (s?.term !== term) return;
87
+ // Find this session by term reference — key may have changed via rename
88
+ let currentName: string | undefined;
89
+ let s: typeof session | undefined;
90
+ for (const [key, entry] of ptySessions) {
91
+ if (entry.term === term) { currentName = key; s = entry; break; }
92
+ }
93
+ if (!currentName || !s) return;
89
94
 
90
95
  // If a resumed session exits quickly (< 5s), signal client to auto-reconnect fresh
91
96
  const elapsed = Date.now() - spawnTime;
92
97
  if (isResume && elapsed < 5000 && exitCode !== 0) {
93
- console.log(`Resume for "${storyName}" failed (exit ${exitCode} in ${elapsed}ms), signaling fresh fallback`);
94
- ptySessions.delete(storyName);
98
+ console.log(`Resume for "${currentName}" failed (exit ${exitCode} in ${elapsed}ms), signaling fresh fallback`);
99
+ ptySessions.delete(currentName);
95
100
  if (s.ws && s.ws.readyState <= 1) {
96
101
  // Close code 4000 = resume-failed, client should auto-reconnect fresh
97
102
  s.ws.close(4000, "resume-failed");
@@ -193,6 +198,32 @@ terminal.delete("/:storyName/discard", (c) => {
193
198
  return c.json({ ok: true });
194
199
  });
195
200
 
201
+ /** POST /api/terminal/rename — rename a session key without killing the process */
202
+ terminal.post("/rename", async (c) => {
203
+ const body = await c.req.json<{ oldName?: string; newName?: string }>().catch(() => ({}));
204
+ const oldName = body.oldName && safeName(body.oldName);
205
+ const newName = body.newName && safeName(body.newName);
206
+ if (!oldName || !newName) return c.json({ error: "Invalid names" }, 400);
207
+ if (oldName === newName) return c.json({ ok: true });
208
+
209
+ const session = ptySessions.get(oldName);
210
+ if (!session) return c.json({ error: "Session not found" }, 404);
211
+
212
+ if (ptySessions.has(newName)) return c.json({ error: "Target session already exists" }, 409);
213
+
214
+ // Move in-memory PTY entry
215
+ ptySessions.delete(oldName);
216
+ ptySessions.set(newName, session);
217
+
218
+ // Update persisted session map: remove old key, store under new key
219
+ const sessionMap = loadSessionMap();
220
+ delete sessionMap[oldName];
221
+ sessionMap[newName] = session.sessionId;
222
+ saveSessionMap(sessionMap);
223
+
224
+ return c.json({ ok: true, sessionId: session.sessionId });
225
+ });
226
+
196
227
  /** POST /api/terminal/stop — kill PTY (legacy, kills default) */
197
228
  terminal.post("/stop", (c) => {
198
229
  const session = ptySessions.get("default");
@@ -150,7 +150,7 @@ export function PreviewPanel({ storyName, fileName, authFetch, onPublish, publis
150
150
  const isGenesis = fileName === "genesis.md";
151
151
  const isPlot = fileName ? /^plot-\d+\.md$/.test(fileName) : false;
152
152
  const isPublished = fileData?.status === "published" || fileData?.status === "published-not-indexed";
153
- const charLimit = isGenesis ? 1000 : isPlot ? 10000 : null;
153
+ const charLimit = (isGenesis || isPlot) ? 10000 : null;
154
154
  // Don't show over-limit warning for already-published files
155
155
  const overLimit = !isPublished && charLimit !== null && charCount > charLimit;
156
156
 
@@ -41,6 +41,7 @@ export function StoriesPage({ token, authFetch }: StoriesPageProps) {
41
41
  const [ratio, setRatio] = useState(loadRatio);
42
42
  const [untitledSessions, setUntitledSessions] = useState<string[]>([]);
43
43
  const knownStoriesRef = useRef<Set<string>>(new Set());
44
+ const renameRef = useRef<((oldName: string, newName: string) => Promise<boolean>) | null>(null);
44
45
  const containerRef = useRef<HTMLDivElement>(null);
45
46
  const dragging = useRef(false);
46
47
 
@@ -85,8 +86,15 @@ export function StoriesPage({ token, authFetch }: StoriesPageProps) {
85
86
  // Detect newly appeared stories
86
87
  for (const name of currentNames) {
87
88
  if (!knownStoriesRef.current.has(name) && untitledSessions.length > 0) {
88
- // New story appeared — transition the oldest untitled session
89
- setUntitledSessions((prev) => prev.slice(1));
89
+ // New story appeared — rename the oldest untitled session to the story name
90
+ const oldName = untitledSessions[0];
91
+ let renamed = false;
92
+ if (renameRef.current) {
93
+ renamed = await renameRef.current(oldName, name).catch(() => false);
94
+ }
95
+ if (renamed) {
96
+ setUntitledSessions((prev) => prev.slice(1));
97
+ }
90
98
  setSelectedStory(name);
91
99
  setSelectedFile(null);
92
100
  }
@@ -330,7 +338,7 @@ export function StoriesPage({ token, authFetch }: StoriesPageProps) {
330
338
 
331
339
  {/* Terminal — sized by ratio of available space */}
332
340
  <div className="min-w-0 border-r border-border" style={{ flex: `${ratio} 0 0` }}>
333
- <TerminalPanel token={token} storyName={selectedStory} authFetch={authFetch} onSelectStory={handleSelectStory} onDestroySession={handleDestroySession} onArchiveStory={handleArchiveStory} confirmedStories={confirmedStories} />
341
+ <TerminalPanel token={token} storyName={selectedStory} authFetch={authFetch} onSelectStory={handleSelectStory} onDestroySession={handleDestroySession} onArchiveStory={handleArchiveStory} confirmedStories={confirmedStories} renameRef={renameRef} />
334
342
  </div>
335
343
 
336
344
  {/* Drag Handle */}
@@ -12,6 +12,7 @@ interface TerminalPanelProps {
12
12
  onDestroySession?: (storyName: string) => void;
13
13
  onArchiveStory?: (storyName: string) => void;
14
14
  confirmedStories?: Set<string>;
15
+ renameRef?: React.RefObject<((oldName: string, newName: string) => Promise<boolean>) | null>;
15
16
  }
16
17
 
17
18
  interface TerminalSession {
@@ -105,7 +106,7 @@ async function deleteScrollback(storyName: string): Promise<void> {
105
106
  // Sessions live outside React state to avoid ref-in-effect lint issues
106
107
  const sessions = new Map<string, TerminalSession>();
107
108
 
108
- export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDestroySession, onArchiveStory, confirmedStories }: TerminalPanelProps) {
109
+ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDestroySession, onArchiveStory, confirmedStories, renameRef }: TerminalPanelProps) {
109
110
  const wrapperRef = useRef<HTMLDivElement>(null);
110
111
  const authFetchRef = useRef(authFetch);
111
112
  const [sessionList, setSessionList] = useState<string[]>([]);
@@ -327,6 +328,50 @@ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDe
327
328
  onDestroySession?.(name);
328
329
  }, [authFetch, onDestroySession]);
329
330
 
331
+ /** Rename a session key (e.g. _new_123 → paper-chair) without killing the PTY.
332
+ * Returns true on success, false on failure. */
333
+ const renameSession = useCallback(async (oldName: string, newName: string): Promise<boolean> => {
334
+ const session = sessions.get(oldName);
335
+ if (!session || sessions.has(newName)) return false;
336
+
337
+ // Rename on the server first
338
+ const res = await authFetchRef.current("/api/terminal/rename", {
339
+ method: "POST",
340
+ headers: { "Content-Type": "application/json" },
341
+ body: JSON.stringify({ oldName, newName }),
342
+ });
343
+ if (!res.ok) return false;
344
+
345
+ // Move in client-side sessions map
346
+ sessions.delete(oldName);
347
+ sessions.set(newName, session);
348
+
349
+ // Migrate scrollback under the new key
350
+ try {
351
+ const data = session.serialize.serialize();
352
+ await deleteScrollback(oldName);
353
+ await saveScrollback(newName, data);
354
+ } catch { /* ignore */ }
355
+
356
+ // Update React state
357
+ setSessionList((prev) => prev.map((s) => (s === oldName ? newName : s)));
358
+ setDisconnected((prev) => {
359
+ if (!prev.has(oldName)) return prev;
360
+ const next = new Set(prev);
361
+ next.delete(oldName);
362
+ next.add(newName);
363
+ return next;
364
+ });
365
+
366
+ return true;
367
+ }, []);
368
+
369
+ // Expose renameSession to parent via ref
370
+ useEffect(() => {
371
+ if (renameRef) renameRef.current = renameSession;
372
+ return () => { if (renameRef) renameRef.current = null; };
373
+ }, [renameRef, renameSession]);
374
+
330
375
  // Auto-spawn + show/hide when story changes
331
376
  useEffect(() => {
332
377
  if (!storyName) return;