@wenathlan/devthink 1.1.13 → 1.1.14

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/CHANGELOG.md CHANGED
@@ -1,4 +1,16 @@
1
1
  # DevThink release notes
2
+ ## 1.1.14 — Route and local identity refinement
3
+
4
+ ### Added
5
+
6
+ | Area | Addition |
7
+ | --- | --- |
8
+ | Portable Home route | Relative static builds preserve the Vite `./` base and resolve the Home route at `/` without treating `.` as a router prefix. |
9
+ | CLI-owned person | The browser now projects the authenticated CLI-owned public user and device identity after pairing instead of storing a second browser identity. |
10
+ | Shared public ID | `devthink identity --id <value>` and the paired web Settings panel update the same local public identifier, with compact 10–15 character validation. |
11
+ | Shared route index | Web and CLI route maps now list the authenticated `GET` and `PUT /identity` operations alongside preferences and workspace routes. |
12
+ | Documentation boundary | Workbench design and deployment notes moved from `/web` to `docs/`, leaving the static application tree free of planning documents. |
13
+
2
14
  ## 1.1.13 — Flat domain consolidation
3
15
 
4
16
  ### Added
@@ -11,6 +23,10 @@
11
23
  | Compact visual grammar | Historical workspace CSS overrides were replaced with one terminal workspace system for entry, tabs, rail, command bar, pairing, controls and error states. |
12
24
  | Control frame | Providers, projects, routes and usage share one explicitly named root control shell without a generic application directory. |
13
25
  | Preserved parity boundary | Web and Ink retain the same safe workspace destinations, settings and local gateway interactions; provider credentials remain CLI-owned. |
26
+ | Portable Home route | Relative Vite builds now retain `./` as a relative base and normalize it to the Home router base, so `/` resolves the first workbench screen in portable static artifacts. |
27
+ | Same local person | A paired browser reads the CLI-owned public user and device IDs through the authenticated gateway instead of creating a second browser profile. |
28
+ | Public ID controls | `devthink identity --id <value>` and the paired web Settings panel update the same local public ID, preserving compact workspace data and temporary browser sessions. |
29
+ | Documentation boundary | Workbench deployment and design records now live under `docs/`, leaving `/web` exclusively for browser application artifacts. |
14
30
 
15
31
  ## 1.1.12 — Page domains and tabbed interaction
16
32
 
package/devthink.ts CHANGED
@@ -8,7 +8,7 @@ import { createMemoryStore, memorySummary } from "./memory.ts";
8
8
  import { listModes, modePrompt, resolveMode } from "./modes.ts";
9
9
  import { listModels, listProviders, streamChat } from "./providers.ts";
10
10
  import { appendMessage, createSession, exportSession, listSessions, loadSession, type Session } from "./session.ts";
11
- import { createPairing, createPairingLink, getIdentity, pairingStatus, revokeBrowserSessions } from "./identity.ts";
11
+ import { createPairing, createPairingLink, getIdentity, pairingStatus, revokeBrowserSessions, setIdentityUserId } from "./identity.ts";
12
12
  import { readPreferences, savePreference } from "./storage.ts";
13
13
  import type { ChatEvent } from "./stream.ts";
14
14
  import { banner, box, colors, formatConfig, formatEvent, statusBar } from "./ui.ts";
@@ -74,7 +74,7 @@ function printHelp(): void {
74
74
  " auth status Show redacted configured credential sources",
75
75
  " auth clear <provider> Remove a stored provider credential",
76
76
  " auth migrate Move legacy config credentials into auth.json",
77
- " identity Show local public user and device identifiers",
77
+ " identity [--id <value>] Show or set the local public user identifier",
78
78
  " pair create Create an automatic one-time link for the web workbench",
79
79
  " pair revoke Revoke current paired browser sessions",
80
80
  " sync status Show local snapshot and remote adapter readiness",
@@ -212,13 +212,15 @@ async function handleAuth(parsed: ParsedArgs, runtime: ReturnType<typeof loadRun
212
212
  throw new Error("Supported auth actions: login, status, clear, migrate.");
213
213
  }
214
214
 
215
- function handleIdentity(runtime: ReturnType<typeof loadRuntime>): void {
216
- console.log(JSON.stringify({ identity: getIdentity(runtime.paths), pairing: pairingStatus(runtime.paths) }, null, 2));
215
+ function handleIdentity(parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>): void {
216
+ const requestedUserId = stringFlag(parsed, "id");
217
+ const identity = requestedUserId ? setIdentityUserId(runtime.paths, requestedUserId) : getIdentity(runtime.paths);
218
+ console.log(JSON.stringify({ identity, pairing: pairingStatus(runtime.paths) }, null, 2));
217
219
  }
218
220
 
219
221
  function handlePairing(parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>): void {
220
222
  const action = parsed.positional[1] || "create";
221
- if (action === "status") return handleIdentity(runtime);
223
+ if (action === "status") return handleIdentity(parsed, runtime);
222
224
  if (action === "revoke") return console.log(`Revoked ${revokeBrowserSessions(runtime.paths)} paired browser session(s).`);
223
225
  if (action !== "create") throw new Error("Supported pair actions: create, status, revoke.");
224
226
  const pairing = createPairing(runtime.paths);
@@ -278,7 +280,7 @@ function handleUsage(runtime: ReturnType<typeof loadRuntime>): void {
278
280
  }
279
281
 
280
282
  function handleRoutes(): void {
281
- console.log(["web: /", "web: /providers", "web: /projects", "web: /routes", "web: /usage", "web: /w/:workspaceId/s/:sessionId/t/:tabId/:sectionId", "gateway: GET /health", "gateway: GET /providers", "gateway: GET /workspaces", "gateway: GET /usage", "gateway: GET /preferences", "gateway: PATCH /preferences", "gateway: POST /sessions", "gateway: POST /chat"].join("\n"));
283
+ console.log(["web: /", "web: /providers", "web: /projects", "web: /routes", "web: /usage", "web: /w/:workspaceId/s/:sessionId/t/:tabId/:sectionId", "gateway: GET /health", "gateway: GET /identity", "gateway: PUT /identity", "gateway: GET /providers", "gateway: GET /workspaces", "gateway: GET /usage", "gateway: GET /preferences", "gateway: PATCH /preferences", "gateway: POST /sessions", "gateway: POST /chat"].join("\n"));
282
284
  }
283
285
 
284
286
  async function interactive(runtime: ReturnType<typeof loadRuntime>): Promise<void> {
@@ -351,7 +353,7 @@ export async function main(argv = process.argv.slice(2)): Promise<void> {
351
353
  if (command === "modes") return console.log(listModes().map((mode) => `${mode.id} ${mode.purpose}`).join("\n"));
352
354
  if (command === "config") return handleConfig(parsed, runtime);
353
355
  if (command === "auth") return handleAuth(parsed, runtime);
354
- if (command === "identity") return handleIdentity(runtime);
356
+ if (command === "identity") return handleIdentity(parsed, runtime);
355
357
  if (command === "pair") return handlePairing(parsed, runtime);
356
358
  if (command === "sync") return handleSync(parsed, runtime);
357
359
  if (command === "gateway") return handleGateway(runtime);
package/identity.ts CHANGED
@@ -13,6 +13,12 @@ const codeLength = 8;
13
13
  const pairingLifetimeMs = 5 * 60 * 1000;
14
14
  const browserSessionLifetimeMs = 15 * 60 * 1000;
15
15
 
16
+ export function normalizePublicUserId(value: string): string {
17
+ const userId = value.trim().toLowerCase();
18
+ if (!/^[a-z][a-z0-9]{9,14}$/.test(userId)) throw new Error("Public user ID must contain 10-15 lowercase letters or numbers and start with a letter.");
19
+ return userId;
20
+ }
21
+
16
22
  function digest(value: string): string {
17
23
  return createHash("sha256").update(value).digest("hex");
18
24
  }
@@ -44,6 +50,20 @@ export function getIdentity(paths: DevThinkPaths): DevThinkIdentity {
44
50
  return identity;
45
51
  }
46
52
 
53
+ /** Updates the public local-person identifier and migrates active local pairing records. */
54
+ export function setIdentityUserId(paths: DevThinkPaths, requestedUserId: string): DevThinkIdentity {
55
+ const current = getIdentity(paths);
56
+ const userId = normalizePublicUserId(requestedUserId);
57
+ if (current.userId === userId) return current;
58
+ const next = { ...current, userId };
59
+ privateWrite(paths.identity, next);
60
+ const store = readStore(paths);
61
+ for (const record of store.pairings) if (record.userId === current.userId) record.userId = userId;
62
+ for (const session of store.sessions) if (session.userId === current.userId) session.userId = userId;
63
+ writeStore(paths, store);
64
+ return next;
65
+ }
66
+
47
67
  function readStore(paths: DevThinkPaths): PairingStore {
48
68
  try {
49
69
  if (existsSync(paths.pairings)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenathlan/devthink",
3
- "version": "1.1.13",
3
+ "version": "1.1.14",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/wenathlan/devthink"
package/server.ts CHANGED
@@ -3,7 +3,7 @@ import { randomInt } from "node:crypto";
3
3
  import { isAllowedOrigin } from "./compatibility.ts";
4
4
  import { listModels, listProviders, streamChat, type ChatMessage } from "./providers.ts";
5
5
  import { ensurePaths, resolvePaths, saveConfig, type DevThinkConfig, type DevThinkPaths } from "./config.ts";
6
- import { consumePairing, getIdentity, pairingStatus, revokeBrowserSessions, verifyBrowserSession } from "./identity.ts";
6
+ import { consumePairing, getIdentity, pairingStatus, revokeBrowserSessions, setIdentityUserId, verifyBrowserSession } from "./identity.ts";
7
7
  import { appendMessage, createSession, createTab, listSessions, loadSession, loadWorkspace, updateTab, type SessionSection } from "./session.ts";
8
8
  import { readPreferences, savePreference } from "./storage.ts";
9
9
 
@@ -138,6 +138,12 @@ async function route(request: IncomingMessage, response: ServerResponse, config:
138
138
  const browserSession = origin ? verifyBrowserSession(paths, bearerToken(request)) : undefined;
139
139
  if (origin && !browserSession) return writeJson(response, 401, { error: "A current browser pairing token is required." }, origin);
140
140
  if (request.method === "GET" && url.pathname === "/identity") return writeJson(response, 200, { identity: getIdentity(paths), pairing: pairingStatus(paths) }, origin);
141
+ if (request.method === "PUT" && url.pathname === "/identity") {
142
+ const body = await readBody(request);
143
+ if (typeof body.userId !== "string") return writeJson(response, 400, { error: "userId is required." }, origin);
144
+ try { return writeJson(response, 200, { identity: setIdentityUserId(paths, body.userId), pairing: pairingStatus(paths) }, origin); }
145
+ catch (error) { return writeJson(response, 400, { error: error instanceof Error ? error.message : "Identity update was rejected." }, origin); }
146
+ }
141
147
  if (request.method === "POST" && url.pathname === "/pairings/revoke") return writeJson(response, 200, { revoked: revokeBrowserSessions(paths) }, origin);
142
148
  if (request.method === "GET" && url.pathname === "/sessions") return writeJson(response, 200, { sessions: listSessions(paths) }, origin);
143
149
  if (request.method === "GET" && url.pathname === "/workspaces") {
package/terminal-ui.tsx CHANGED
@@ -1,9 +1,10 @@
1
- /** Design: DevThink v1.1.13 — Ink shares the compact local workspace grammar, destinations, settings and command language with the flat React workbench. */
1
+ /** Design: DevThink v1.1.14 — Ink shares the compact local workspace grammar, identity, destinations, settings and command language with the paired React workbench. */
2
2
  import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
3
3
  import { useMemo, useState } from "react";
4
4
  import { listProviders } from "./providers.ts";
5
5
  import { listSessions, type Session } from "./session.ts";
6
6
  import { readPreferences, savePreference } from "./storage.ts";
7
+ import { getIdentity } from "./identity.ts";
7
8
  import type { DevThinkConfig, DevThinkPaths } from "./config.ts";
8
9
  import type { ChatEvent } from "./stream.ts";
9
10
  import { isWorkspaceDestination, workspaceDestination, workspaceDestinations, type WorkspaceDestination } from "./workspace.ts";
@@ -20,9 +21,9 @@ function clip(value: string, width: number): string {
20
21
  return value.length > width ? `${value.slice(0, Math.max(0, width - 1))}…` : value;
21
22
  }
22
23
 
23
- function TerminalEntry({ draft, provider, width }: { draft: string; provider: string; width: number }) {
24
+ function TerminalEntry({ draft, provider, userId, width }: { draft: string; provider: string; userId: string; width: number }) {
24
25
  return <Box flexDirection="column" width="100%" height="100%" minHeight={20} paddingX={2} paddingY={1} justifyContent="space-between">
25
- <Box justifyContent="space-between"><Text color="#807970">✦ DEVTHINK</Text><Text color="#8ab4f8">LOCAL-FIRST WORKBENCH</Text></Box>
26
+ <Box justifyContent="space-between"><Text color="#807970">✦ DEVTHINK</Text><Text color="#8ab4f8">{userId}</Text></Box>
26
27
  <Box flexDirection="column" alignItems="center"><Box flexDirection="column" alignItems="center">{entryArt.map((line, index) => <Text key={`${line}-${index}`} color="#382c23">{line}</Text>)}</Box><Box marginTop={-1}><Text color="#ff5f00" bold>DEVTHINK</Text></Box><Box marginTop={1}><Text color="#746f66">begin with one intention</Text></Box><Box marginTop={2}><Text color="#282724">┌{"─".repeat(Math.max(16, width - 2))}┐</Text></Box><Text color="#e8e1d7">│ <Text color="#ff5f00">›_ </Text><Text color={draft ? "#e8e1d7" : "#746f66"}>{clip(draft || "Ask anything…", Math.max(12, width - 22))}</Text><Text color="#8ab4f8"> {provider}</Text><Text color="#ff5f00"> [open]</Text> │</Text><Text color="#282724">└{"─".repeat(Math.max(16, width - 2))}┘</Text></Box>
27
28
  <Text color="#746f66">enter opens a local workspace · esc exits</Text>
28
29
  </Box>;
@@ -45,7 +46,8 @@ function viewRows(view: WorkspaceDestination, runtime: Runtime, session: Session
45
46
  if (view === "routes") return ["GET /health", "GET /providers", "GET /workspaces", "GET /usage", "GET /preferences", "PATCH /preferences", "POST /sessions", "POST /chat"];
46
47
  if (view === "settings") {
47
48
  const preferences = Object.fromEntries(Object.entries(readPreferences(runtime.paths)).map(([key, preference]) => [key, preference.value]));
48
- return [`theme ${preferences.theme || "dark"}`, `railMode ${preferences.railMode || "auto"}`, `interfaceZoom ${preferences.interfaceZoom || "100"}`, "edit: /settings set theme dark|light", "edit: /settings set railMode always|auto|off", "edit: /settings set interfaceZoom 80-150", "credentials remain in ~/.config/devthink/auth.json"];
49
+ const identity = getIdentity(runtime.paths);
50
+ return [`userId ${identity.userId}`, `deviceId ${identity.deviceId}`, `theme ${preferences.theme || "dark"}`, `railMode ${preferences.railMode || "auto"}`, `interfaceZoom ${preferences.interfaceZoom || "100"}`, "edit identity: devthink identity --id <10-15 lowercase chars>", "edit: /settings set theme dark|light", "edit: /settings set railMode always|auto|off", "edit: /settings set interfaceZoom 80-150", "credentials remain in ~/.config/devthink/auth.json"];
49
51
  }
50
52
  if (session) return [`workspace ${session.workspaceId}`, `session ${session.id}`, `tab ${session.activeTabId}`];
51
53
  return ["Describe the next feature, bug or piece of work below."];
@@ -63,6 +65,7 @@ function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; ex
63
65
  const [pending, setPending] = useState(false);
64
66
  const [preferenceRevision, setPreferenceRevision] = useState(0);
65
67
  const provider = runtime.config.activeProvider || runtime.config.provider || "local";
68
+ const identity = getIdentity(runtime.paths);
66
69
  const model = runtime.config.activeModel || runtime.config.model || "not configured";
67
70
  const compact = columns < 100;
68
71
  const terminalWidth = Math.max(38, columns - (compact ? 2 : 6));
@@ -127,10 +130,10 @@ function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; ex
127
130
  if (!key.ctrl && !key.meta && input) setDraft((current) => current + input);
128
131
  });
129
132
 
130
- if (!entered) return <TerminalEntry draft={draft} provider={provider} width={terminalWidth} />;
133
+ if (!entered) return <TerminalEntry draft={draft} provider={provider} userId={identity.userId} width={terminalWidth} />;
131
134
 
132
135
  return <Box flexDirection="column" width="100%" height="100%" minHeight={20} paddingX={compact ? 1 : 3} paddingY={1}>
133
- <Box justifyContent="space-between"><Text color="#a19a91">✦ DEVTHINK <Text color="#746f66">/ {workspaceDestination(view).label}</Text></Text><Text color="#8ab4f8">◉ {provider.toUpperCase()}</Text></Box>
136
+ <Box justifyContent="space-between"><Text color="#a19a91">✦ DEVTHINK <Text color="#746f66">/ {workspaceDestination(view).label}</Text></Text><Text color="#8ab4f8">◉ {identity.userId}</Text></Box>
134
137
  <Text color="#282724">{"─".repeat(terminalWidth)}</Text>
135
138
  <Text color="#746f66">{clip(tabLine, terminalWidth)}</Text>
136
139
  <Box marginTop={1}><Text color="#e8e1d7">{workspaceDestination(view).glyph} {workspaceDestination(view).label.toUpperCase()} <Text color="#746f66">· {clip(model, 22)}</Text></Text></Box>