@tianshu-ai/tianshu 0.3.52 → 0.3.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/boot/idle-runner.d.ts +15 -0
  3. package/packages/server/dist/boot/idle-runner.d.ts.map +1 -0
  4. package/packages/server/dist/boot/idle-runner.js +118 -0
  5. package/packages/server/dist/boot/idle-runner.js.map +1 -0
  6. package/packages/server/dist/boot/routes-channels.d.ts +15 -0
  7. package/packages/server/dist/boot/routes-channels.d.ts.map +1 -0
  8. package/packages/server/dist/boot/routes-channels.js +150 -0
  9. package/packages/server/dist/boot/routes-channels.js.map +1 -0
  10. package/packages/server/dist/boot/routes-core.d.ts +10 -0
  11. package/packages/server/dist/boot/routes-core.d.ts.map +1 -0
  12. package/packages/server/dist/boot/routes-core.js +106 -0
  13. package/packages/server/dist/boot/routes-core.js.map +1 -0
  14. package/packages/server/dist/boot/static-spa.d.ts +12 -0
  15. package/packages/server/dist/boot/static-spa.d.ts.map +1 -0
  16. package/packages/server/dist/boot/static-spa.js +86 -0
  17. package/packages/server/dist/boot/static-spa.js.map +1 -0
  18. package/packages/server/dist/boot/ws-upgrade.d.ts +16 -0
  19. package/packages/server/dist/boot/ws-upgrade.d.ts.map +1 -0
  20. package/packages/server/dist/boot/ws-upgrade.js +115 -0
  21. package/packages/server/dist/boot/ws-upgrade.js.map +1 -0
  22. package/packages/server/dist/channels/index.d.ts +2 -0
  23. package/packages/server/dist/channels/index.d.ts.map +1 -1
  24. package/packages/server/dist/channels/index.js +1 -0
  25. package/packages/server/dist/channels/index.js.map +1 -1
  26. package/packages/server/dist/channels/router.js +17 -49
  27. package/packages/server/dist/channels/router.js.map +1 -1
  28. package/packages/server/dist/channels/stream-sink.d.ts +22 -0
  29. package/packages/server/dist/channels/stream-sink.d.ts.map +1 -0
  30. package/packages/server/dist/channels/stream-sink.js +79 -0
  31. package/packages/server/dist/channels/stream-sink.js.map +1 -0
  32. package/packages/server/dist/chat/agent-loop.d.ts.map +1 -1
  33. package/packages/server/dist/chat/agent-loop.js +2 -1
  34. package/packages/server/dist/chat/agent-loop.js.map +1 -1
  35. package/packages/server/dist/chat/handler.d.ts.map +1 -1
  36. package/packages/server/dist/chat/handler.js +2 -1
  37. package/packages/server/dist/chat/handler.js.map +1 -1
  38. package/packages/server/dist/chat/require-home-dir.d.ts +7 -0
  39. package/packages/server/dist/chat/require-home-dir.d.ts.map +1 -0
  40. package/packages/server/dist/chat/require-home-dir.js +40 -0
  41. package/packages/server/dist/chat/require-home-dir.js.map +1 -0
  42. package/packages/server/dist/index.js +39 -536
  43. package/packages/server/dist/index.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tianshu-ai/tianshu",
3
- "version": "0.3.52",
3
+ "version": "0.3.53",
4
4
  "description": "An open AI agent platform with a sidecar browser. Built in public.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/tianshu-ai/tianshu",
@@ -0,0 +1,15 @@
1
+ import type { GlobalOps } from "../core/global-ops.js";
2
+ import type { PluginRegistry } from "../core/plugins/registry.js";
3
+ export interface InstallIdleRunnerDeps {
4
+ globalOps: GlobalOps;
5
+ pluginRegistry: PluginRegistry;
6
+ }
7
+ /**
8
+ * Install the idle-runner. Idempotent in practice (bindIdleRunner is
9
+ * a one-shot module setter), but conceptually call once at boot.
10
+ *
11
+ * Deps are captured by closure so this module doesn't need a
12
+ * mutable singleton.
13
+ */
14
+ export declare function installIdleRunner(deps: InstallIdleRunnerDeps): void;
15
+ //# sourceMappingURL=idle-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idle-runner.d.ts","sourceRoot":"","sources":["../../src/boot/idle-runner.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAGlE,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,EAAE,cAAc,CAAC;CAChC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI,CAyGnE"}
@@ -0,0 +1,118 @@
1
+ // Wire the session-inbox idle runner.
2
+ //
3
+ // When a worker pool finishes a task and the parent chat session is
4
+ // idle (no active harness), the inbox module kicks a background
5
+ // `runPrompt` turn so the agent reacts to the notification
6
+ // immediately instead of waiting for the user to send something.
7
+ //
8
+ // We bind once at startup (`bindIdleRunner` is process-wide) so the
9
+ // inbox module doesn't have to import the host registry directly. The
10
+ // runner closure resolves the tenant from globalOps at call time \u2014
11
+ // the session row already lives in some tenant DB; the closure is
12
+ // tenant-agnostic.
13
+ //
14
+ // Two delivery paths matter inside the runner:
15
+ // - webchat session \u2192 broadcastToUser to every open WS tab
16
+ // - channel-bound session \u2192 buildChannelStreamSink \u2192 channelHub
17
+ //
18
+ // Without that split a background turn on a wechat-bound session ends
19
+ // up only on the user's webchat tabs (if any) and never crosses back
20
+ // to the platform user. See memory/2026-06-27 for the original bug.
21
+ import { bindIdleRunner } from "../chat/session-inbox.js";
22
+ import { runPrompt } from "../chat/handler.js";
23
+ import { broadcastToUser } from "../chat/active-harnesses.js";
24
+ import { buildChannelStreamSink, channelHub } from "../channels/index.js";
25
+ /**
26
+ * Install the idle-runner. Idempotent in practice (bindIdleRunner is
27
+ * a one-shot module setter), but conceptually call once at boot.
28
+ *
29
+ * Deps are captured by closure so this module doesn't need a
30
+ * mutable singleton.
31
+ */
32
+ export function installIdleRunner(deps) {
33
+ const { globalOps, pluginRegistry } = deps;
34
+ bindIdleRunner(async ({ sessionId, userId, promptText }) => {
35
+ // Find which tenant owns this session, and pick up the channel
36
+ // tagging columns at the same time so we know whether to fan
37
+ // replies out to WS tabs (webchat) or push them through the
38
+ // channel adapter (wechat / telegram / ...). If no tenant
39
+ // claims this session, give up silently \u2014 the inbox row stays
40
+ // delivered=false and will be flushed on the next user prompt.
41
+ let owningCtx = null;
42
+ let channelBindingId = null;
43
+ let channelChatId = null;
44
+ for (const tenantId of globalOps.list()) {
45
+ const ctx = globalOps.open(tenantId);
46
+ const row = ctx.db
47
+ .prepare(`SELECT id, channel_binding_id, channel_chat_id
48
+ FROM sessions WHERE id = ?`)
49
+ .get(sessionId);
50
+ if (row) {
51
+ owningCtx = ctx;
52
+ channelBindingId = row.channel_binding_id;
53
+ channelChatId = row.channel_chat_id;
54
+ break;
55
+ }
56
+ }
57
+ if (!owningCtx) {
58
+ console.warn(`[idle-runner] no tenant found for session ${sessionId}; skipping`);
59
+ return;
60
+ }
61
+ const ctx = owningCtx;
62
+ const isChannelSession = channelBindingId !== null && channelChatId !== null;
63
+ const channelSink = isChannelSession
64
+ ? buildChannelStreamSink({ sessionId, userId })
65
+ : null;
66
+ let errorReason = "";
67
+ const send = (msg) => {
68
+ if (channelSink) {
69
+ channelSink.push(msg);
70
+ }
71
+ else {
72
+ broadcastToUser(userId, msg);
73
+ }
74
+ };
75
+ // Wedge guard: a stuck provider can't pin the inbox queue
76
+ // forever. No user-facing abort button for an inbox turn \u2014
77
+ // generous deadline only.
78
+ const controller = new AbortController();
79
+ const deadline = setTimeout(() => controller.abort(), 5 * 60 * 1000);
80
+ try {
81
+ await runPrompt({
82
+ ctx,
83
+ userId,
84
+ send,
85
+ content: promptText,
86
+ signal: controller.signal,
87
+ pluginRegistry,
88
+ homeDir: ctx.workspaceDir,
89
+ });
90
+ }
91
+ catch (err) {
92
+ errorReason = err instanceof Error ? err.message : String(err);
93
+ }
94
+ finally {
95
+ clearTimeout(deadline);
96
+ }
97
+ if (channelSink && channelBindingId && channelChatId) {
98
+ const sinkError = channelSink.getErrorReason();
99
+ if (sinkError)
100
+ errorReason = sinkError;
101
+ for (const body of channelSink.assistantQueue) {
102
+ try {
103
+ await channelHub.send(channelBindingId, {
104
+ target: channelChatId,
105
+ text: body,
106
+ });
107
+ }
108
+ catch (err) {
109
+ console.error(`[idle-runner] adapter send failed (${channelBindingId} \u2192 ${channelChatId}): ${err instanceof Error ? err.message : String(err)}`);
110
+ }
111
+ }
112
+ if (errorReason.length > 0) {
113
+ console.warn(`[idle-runner] background turn errored on channel session ${sessionId}: ${errorReason}`);
114
+ }
115
+ }
116
+ });
117
+ }
118
+ //# sourceMappingURL=idle-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idle-runner.js","sourceRoot":"","sources":["../../src/boot/idle-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { Express } from "express";
2
+ import type { ChannelBinding } from "../channels/index.js";
3
+ import type { ChannelBindingView } from "@tianshu-ai/plugin-sdk";
4
+ /** Map an internal binding row to the SDK-shaped view. Same wire
5
+ * shape host.channelBindings.list() emits, so plugin UIs and admin
6
+ * routes don't diverge. */
7
+ export declare function toView(row: ChannelBinding): ChannelBindingView;
8
+ /**
9
+ * Mount all `/api/channel-{sessions,bindings}/*` routes on the
10
+ * provided Express app. Call once during boot, after the tenant
11
+ * middleware has been installed (these routes assume `req.ctx`
12
+ * exists).
13
+ */
14
+ export declare function mountChannelRoutes(app: Express): void;
15
+ //# sourceMappingURL=routes-channels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-channels.d.ts","sourceRoot":"","sources":["../../src/boot/routes-channels.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;4BAE4B;AAC5B,wBAAgB,MAAM,CAAC,GAAG,EAAE,cAAc,GAAG,kBAAkB,CAe9D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAUrD"}
@@ -0,0 +1,150 @@
1
+ // Channel admin routes: read-only `/api/channel-sessions/*` for the
2
+ // sidebar + UI controls, plus `PATCH /api/channel-bindings/:id/model`
3
+ // so a user can flip the LLM behind their wechat / telegram thread
4
+ // without re-binding.
5
+ //
6
+ // These all live behind the tenant middleware so `req.ctx` is
7
+ // available; each route enforces user scoping (sessions.user_id /
8
+ // bindings.owner_user_id) so two users in the same tenant can't read
9
+ // each other's channel state.
10
+ import { getBinding, updateBinding } from "../channels/index.js";
11
+ /** Map an internal binding row to the SDK-shaped view. Same wire
12
+ * shape host.channelBindings.list() emits, so plugin UIs and admin
13
+ * routes don't diverge. */
14
+ export function toView(row) {
15
+ return {
16
+ id: row.id,
17
+ tenantId: row.tenantId,
18
+ ownerUserId: row.ownerUserId,
19
+ channelId: row.channelId,
20
+ pluginId: row.pluginId,
21
+ displayName: row.displayName,
22
+ enabled: row.enabled,
23
+ status: row.status,
24
+ statusDetail: row.statusDetail,
25
+ config: row.config,
26
+ createdAt: row.createdAt,
27
+ updatedAt: row.updatedAt,
28
+ };
29
+ }
30
+ /**
31
+ * Mount all `/api/channel-{sessions,bindings}/*` routes on the
32
+ * provided Express app. Call once during boot, after the tenant
33
+ * middleware has been installed (these routes assume `req.ctx`
34
+ * exists).
35
+ */
36
+ export function mountChannelRoutes(app) {
37
+ app.get("/api/channel-sessions", listChannelSessions);
38
+ app.get("/api/channel-sessions/:sessionId/binding", getChannelSessionBinding);
39
+ app.patch("/api/channel-bindings/:bindingId/model", patchChannelBindingModel);
40
+ }
41
+ function listChannelSessions(req, res) {
42
+ if (!req.ctx) {
43
+ res.status(500).json({ error: "no_ctx" });
44
+ return;
45
+ }
46
+ // Scope to the calling user: channel sessions are personal (one
47
+ // user's wechat scan shouldn't surface to other users on the same
48
+ // tenant). user_id on the session row already tracks this because
49
+ // ensureChannelSession stamps it from the binding's owner_user_id.
50
+ const rows = req.ctx.tenant.db
51
+ .prepare(`SELECT id, channel_id, channel_chat_id, channel_binding_id,
52
+ title, created_at
53
+ FROM sessions
54
+ WHERE channel_id IS NOT NULL
55
+ AND kind = 'user'
56
+ AND user_id = ?
57
+ ORDER BY created_at DESC`)
58
+ .all(req.ctx.userId);
59
+ res.json({
60
+ sessions: rows.map((r) => ({
61
+ id: r.id,
62
+ channelId: r.channel_id,
63
+ channelChatId: r.channel_chat_id,
64
+ channelBindingId: r.channel_binding_id,
65
+ title: r.title,
66
+ createdAt: r.created_at,
67
+ })),
68
+ });
69
+ }
70
+ /**
71
+ * Return the binding row a channel session belongs to. Channel UIs
72
+ * use this to populate per-session controls (model selector, etc.)
73
+ * keyed off the session the user is viewing. Filtered by user_id so
74
+ * users can't read each other's bindings.
75
+ */
76
+ function getChannelSessionBinding(req, res) {
77
+ if (!req.ctx) {
78
+ res.status(500).json({ error: "no_ctx" });
79
+ return;
80
+ }
81
+ const rawSid = req.params.sessionId;
82
+ const sessionId = Array.isArray(rawSid) ? rawSid[0] : rawSid;
83
+ if (!sessionId) {
84
+ res.status(400).json({ error: "missing session id" });
85
+ return;
86
+ }
87
+ const row = req.ctx.tenant.db
88
+ .prepare(`SELECT channel_binding_id AS binding_id
89
+ FROM sessions
90
+ WHERE id = ? AND user_id = ?
91
+ LIMIT 1`)
92
+ .get(sessionId, req.ctx.userId);
93
+ if (!row?.binding_id) {
94
+ res.status(404).json({ error: "no binding for session" });
95
+ return;
96
+ }
97
+ const binding = getBinding(req.ctx.tenant.db, row.binding_id);
98
+ if (!binding ||
99
+ binding.tenantId !== req.ctx.tenant.tenantId ||
100
+ binding.ownerUserId !== req.ctx.userId) {
101
+ res.status(404).json({ error: "binding not found" });
102
+ return;
103
+ }
104
+ res.json({
105
+ binding: {
106
+ id: binding.id,
107
+ channelId: binding.channelId,
108
+ modelId: typeof binding.config.modelId === "string" &&
109
+ binding.config.modelId.trim().length > 0
110
+ ? binding.config.modelId.trim()
111
+ : null,
112
+ },
113
+ });
114
+ }
115
+ /**
116
+ * Update a binding's model. PATCH so the body is the partial we
117
+ * want to merge. Only `modelId` is patchable through this route
118
+ * today; future channel-config edits can land on the same surface.
119
+ */
120
+ function patchChannelBindingModel(req, res) {
121
+ if (!req.ctx) {
122
+ res.status(500).json({ error: "no_ctx" });
123
+ return;
124
+ }
125
+ const rawBid = req.params.bindingId;
126
+ const bindingId = Array.isArray(rawBid) ? rawBid[0] : rawBid;
127
+ if (!bindingId) {
128
+ res.status(400).json({ error: "missing binding id" });
129
+ return;
130
+ }
131
+ const body = (req.body ?? {});
132
+ const newModelId = typeof body.modelId === "string" && body.modelId.trim().length > 0
133
+ ? body.modelId.trim()
134
+ : null;
135
+ const binding = getBinding(req.ctx.tenant.db, bindingId);
136
+ if (!binding ||
137
+ binding.tenantId !== req.ctx.tenant.tenantId ||
138
+ binding.ownerUserId !== req.ctx.userId) {
139
+ res.status(404).json({ error: "binding not found" });
140
+ return;
141
+ }
142
+ const nextConfig = { ...binding.config };
143
+ if (newModelId)
144
+ nextConfig.modelId = newModelId;
145
+ else
146
+ delete nextConfig.modelId;
147
+ updateBinding(req.ctx.tenant.db, bindingId, { config: nextConfig });
148
+ res.json({ ok: true, modelId: newModelId });
149
+ }
150
+ //# sourceMappingURL=routes-channels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-channels.js","sourceRoot":"","sources":["../../src/boot/routes-channels.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { Express } from "express";
2
+ import type { PluginRegistry } from "../core/plugins/registry.js";
3
+ export interface MountCoreRoutesDeps {
4
+ /** Registry used by /api/tools and /api/skills to enumerate the
5
+ * current tenant's catalog. Closures resolve it at request time
6
+ * so plugin enable/disable cycles take effect immediately. */
7
+ pluginRegistry: PluginRegistry;
8
+ }
9
+ export declare function mountCoreRoutes(app: Express, deps: MountCoreRoutesDeps): void;
10
+ //# sourceMappingURL=routes-core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-core.d.ts","sourceRoot":"","sources":["../../src/boot/routes-core.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAG1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,MAAM,WAAW,mBAAmB;IAClC;;mEAE+D;IAC/D,cAAc,EAAE,cAAc,CAAC;CAChC;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,mBAAmB,GACxB,IAAI,CA0GN"}
@@ -0,0 +1,106 @@
1
+ // Core read-only API: `/api/me`, `/api/models`, `/api/tools`,
2
+ // `/api/skills`. These power the chat shell's identity badge,
3
+ // model picker, and worker-agents allow-list pickers.
4
+ //
5
+ // All four assume tenant middleware has run (req.ctx is set); each
6
+ // scopes to the calling user when appropriate. Side-effect-free \u2014
7
+ // no DB writes, no plugin state mutation.
8
+ import { DEV_TENANT_ID, DEV_USER_ID } from "../core/dev-mode.js";
9
+ import { listModels, getDefaultModel } from "../core/llm.js";
10
+ export function mountCoreRoutes(app, deps) {
11
+ const { pluginRegistry } = deps;
12
+ app.get("/api/me", (req, res) => {
13
+ if (!req.ctx) {
14
+ res.status(500).json({ error: "no_ctx" });
15
+ return;
16
+ }
17
+ const { tenant, userId } = req.ctx;
18
+ const def = getDefaultModel(tenant.config);
19
+ res.json({
20
+ tenantId: tenant.tenantId,
21
+ userId,
22
+ config: { branding: tenant.config.branding ?? null },
23
+ defaultModel: def
24
+ ? { id: def.id, name: def.name, provider: def.providerId }
25
+ : null,
26
+ devTenant: tenant.tenantId === DEV_TENANT_ID && userId === DEV_USER_ID,
27
+ });
28
+ });
29
+ app.get("/api/models", (req, res) => {
30
+ if (!req.ctx) {
31
+ res.status(500).json({ error: "no_ctx" });
32
+ return;
33
+ }
34
+ const list = listModels(req.ctx.tenant.config).map((m) => ({
35
+ id: m.id,
36
+ name: m.name,
37
+ provider: m.providerId,
38
+ group: m.group ?? null,
39
+ contextWindow: m.contextWindow,
40
+ reasoning: m.reasoning,
41
+ }));
42
+ res.json({
43
+ models: list,
44
+ defaultModel: req.ctx.tenant.config.defaultModel ?? null,
45
+ });
46
+ });
47
+ /**
48
+ * Tool catalog for the current tenant. Used by the worker-agents
49
+ * settings page to render an allow-list picker instead of the
50
+ * old comma-separated freetext field.
51
+ *
52
+ * Returns ALL tools the host registry knows about (host built-ins
53
+ * + every active plugin's contributions). Per-agent allow-list
54
+ * filtering happens at the worker; this endpoint is just the
55
+ * universe to pick from.
56
+ */
57
+ app.get("/api/tools", (req, res) => {
58
+ if (!req.ctx) {
59
+ res.status(500).json({ error: "no_ctx" });
60
+ return;
61
+ }
62
+ const entries = pluginRegistry.toolsForTenant(req.ctx.tenant.tenantId);
63
+ // De-dupe by tool name; if two plugins shipped the same name we
64
+ // still only show it once. Stable sort by name for the UI.
65
+ const byName = new Map();
66
+ for (const { pluginId, tool } of entries) {
67
+ if (byName.has(tool.schema.name))
68
+ continue;
69
+ byName.set(tool.schema.name, {
70
+ name: tool.schema.name,
71
+ description: tool.schema.description ?? "",
72
+ pluginId,
73
+ });
74
+ }
75
+ const tools = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
76
+ res.json({ tools });
77
+ });
78
+ /**
79
+ * Skill catalog for the current tenant. Same role as /api/tools \u2014
80
+ * the universe of skills available (host-shipped + plugin-shipped)
81
+ * for the worker-agents allow-list picker.
82
+ */
83
+ app.get("/api/skills", (req, res) => {
84
+ if (!req.ctx) {
85
+ res.status(500).json({ error: "no_ctx" });
86
+ return;
87
+ }
88
+ const skills = pluginRegistry.skillsForTenant(req.ctx.tenant.tenantId);
89
+ // Same shape as /api/tools \u2014 just the bits the picker UI needs.
90
+ // Description is the SKILL.md frontmatter so the picker can render
91
+ // a tooltip; the body markdown stays server-side.
92
+ const out = skills
93
+ .map((s) => ({
94
+ name: s.name,
95
+ description: s.description,
96
+ pluginId: s.source.pluginId,
97
+ // Surface frontmatter `scope:` so the worker-agents page can
98
+ // hide \"scope: main\" skills from a worker's effective list.
99
+ // Undefined = visible to both.
100
+ scope: s.scope,
101
+ }))
102
+ .sort((a, b) => a.name.localeCompare(b.name));
103
+ res.json({ skills: out });
104
+ });
105
+ }
106
+ //# sourceMappingURL=routes-core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-core.js","sourceRoot":"","sources":["../../src/boot/routes-core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Express } from "express";
2
+ /**
3
+ * Mount the static web dist + SPA fallback when configured. Returns
4
+ * true if hosting was activated, false in dev mode / on failure (so
5
+ * the caller can log appropriately).
6
+ */
7
+ export declare function mountStaticSpa(app: Express): Promise<boolean>;
8
+ /** Whether this process is the one hosting the SPA. Cheap probe,
9
+ * same check `mountStaticSpa` uses internally; callers that need
10
+ * to publish the URL out-of-band consult this. */
11
+ export declare function isSpaHosted(): boolean;
12
+ //# sourceMappingURL=static-spa.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-spa.d.ts","sourceRoot":"","sources":["../../src/boot/static-spa.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGvC;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CA6CnE;AAED;;mDAEmD;AACnD,wBAAgB,WAAW,IAAI,OAAO,CAIrC"}
@@ -0,0 +1,86 @@
1
+ // Optional in-process static SPA hosting.
2
+ //
3
+ // Two deployment modes:
4
+ //
5
+ // Dev (`npm run dev` from a checkout): vite hosts the web on its
6
+ // own port (5183 by default). TIANSHU_WEB_DIST is unset, this
7
+ // module is a no-op.
8
+ //
9
+ // Production / global install: the wizard's launchd plist sets
10
+ // TIANSHU_WEB_DIST to the bundled web dist directory and the
11
+ // server hosts the static files itself \u2014 the user only needs
12
+ // one port (3110) instead of two processes.
13
+ //
14
+ // Mount AFTER every `/api/*` and `/ws` handler so the SPA fallback
15
+ // catch-all only fires for non-API requests. The fallback (any
16
+ // unknown path \u2192 index.html) is what makes
17
+ // `/tenants/foo/users/bar/` work without a real route on the
18
+ // filesystem.
19
+ //
20
+ // Why we read index.html into a buffer rather than using
21
+ // `res.sendFile()`: under Express 5 + Node 22+'s send module,
22
+ // sendFile() with an absolute path consistently 404'd on our setup
23
+ // even though `existsSync(file)` returned true. We don't fully
24
+ // understand the resolution path send takes; bypassing it with a
25
+ // direct buffer write is simple and works the same on every Node
26
+ // version we test.
27
+ import express from "express";
28
+ /**
29
+ * Mount the static web dist + SPA fallback when configured. Returns
30
+ * true if hosting was activated, false in dev mode / on failure (so
31
+ * the caller can log appropriately).
32
+ */
33
+ export async function mountStaticSpa(app) {
34
+ const webDistRaw = process.env.TIANSHU_WEB_DIST;
35
+ if (!webDistRaw || webDistRaw.length === 0)
36
+ return false;
37
+ try {
38
+ const path = await import("node:path");
39
+ const fs = await import("node:fs");
40
+ const webDist = path.resolve(webDistRaw);
41
+ if (!fs.existsSync(path.join(webDist, "index.html"))) {
42
+ // eslint-disable-next-line no-console
43
+ console.warn(`[tianshu] TIANSHU_WEB_DIST=${webDist} but no index.html there; ` +
44
+ "skipping static UI mount.");
45
+ return false;
46
+ }
47
+ // Two-layer handler:
48
+ // 1. express.static handles `/index.html`, `/assets/*`, etc.
49
+ // fallthrough: true so requests it doesn't recognise
50
+ // cascade to the next middleware.
51
+ // 2. SPA fallback: any GET request that wasn't /api or /ws
52
+ // and wasn't a static asset \u2192 serve the pre-read
53
+ // index.html bytes. The React router on the client decides
54
+ // what to render.
55
+ app.use(express.static(webDist, { index: false, fallthrough: true }));
56
+ const indexHtml = fs.readFileSync(path.join(webDist, "index.html"));
57
+ app.use((req, res, next) => {
58
+ if (req.path.startsWith("/api/"))
59
+ return next();
60
+ if (req.path === "/api")
61
+ return next();
62
+ if (req.path.startsWith("/ws"))
63
+ return next();
64
+ if (req.method !== "GET" && req.method !== "HEAD")
65
+ return next();
66
+ // Anything else \u2014 /, /tenants/x/users/y/, /admin/foo \u2014 gets
67
+ // index.html. The SPA's router handles it.
68
+ res.type("html").send(indexHtml);
69
+ });
70
+ // eslint-disable-next-line no-console
71
+ console.log(`[tianshu] serving web UI from ${webDist}`);
72
+ return true;
73
+ }
74
+ catch (err) {
75
+ // eslint-disable-next-line no-console
76
+ console.warn(`[tianshu] failed to mount static web dist: ${err instanceof Error ? err.message : String(err)}`);
77
+ return false;
78
+ }
79
+ }
80
+ /** Whether this process is the one hosting the SPA. Cheap probe,
81
+ * same check `mountStaticSpa` uses internally; callers that need
82
+ * to publish the URL out-of-band consult this. */
83
+ export function isSpaHosted() {
84
+ return Boolean(process.env.TIANSHU_WEB_DIST && process.env.TIANSHU_WEB_DIST.length > 0);
85
+ }
86
+ //# sourceMappingURL=static-spa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-spa.js","sourceRoot":"","sources":["../../src/boot/static-spa.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { Server as HttpServer } from "node:http";
2
+ import { WebSocketServer } from "ws";
3
+ import type { GlobalOps } from "../core/global-ops.js";
4
+ import type { PluginRegistry } from "../core/plugins/registry.js";
5
+ export interface InstallChatWebSocketDeps {
6
+ server: HttpServer;
7
+ globalOps: GlobalOps;
8
+ pluginRegistry: PluginRegistry;
9
+ }
10
+ /**
11
+ * Install the `/ws` WebSocketServer on the provided HTTP server.
12
+ * Returns the WebSocketServer for the caller to keep alive / close
13
+ * on shutdown.
14
+ */
15
+ export declare function installChatWebSocket(deps: InstallChatWebSocketDeps): WebSocketServer;
16
+ //# sourceMappingURL=ws-upgrade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-upgrade.d.ts","sourceRoot":"","sources":["../../src/boot/ws-upgrade.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAOrC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAGlE,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,EAAE,cAAc,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,wBAAwB,GAC7B,eAAe,CAyGjB"}