@wenathlan/devthink 1.1.12 → 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 +29 -0
- package/devthink.ts +9 -7
- package/identity.ts +20 -0
- package/package.json +1 -1
- package/server.ts +7 -1
- package/terminal-ui.tsx +9 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
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
|
+
|
|
14
|
+
## 1.1.13 — Flat domain consolidation
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
| Area | Addition |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| Flat application entry | `web/App.tsx` now owns React mounting, routing, the error boundary, global stylesheet import and browser notification surface; `main.tsx` is no longer required. |
|
|
21
|
+
| Domain-only web tree | Generic `app`, `contexts`, `hooks`, `lib`, `primitives`, `public`, `components`, `pages` and `src` directories have been removed from the workbench. |
|
|
22
|
+
| Local ownership | Page-level controls now use native semantic elements, while the Routes map owns its only callback and class-join helpers directly. |
|
|
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. |
|
|
24
|
+
| Control frame | Providers, projects, routes and usage share one explicitly named root control shell without a generic application directory. |
|
|
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. |
|
|
30
|
+
|
|
2
31
|
## 1.1.12 — Page domains and tabbed interaction
|
|
3
32
|
|
|
4
33
|
### Added
|
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
|
|
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
|
-
|
|
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
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.
|
|
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">
|
|
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
|
-
|
|
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">◉ {
|
|
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>
|