specrails-desktop 2.54.0 → 2.55.0

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 (31) hide show
  1. package/client/dist/assets/{ActivityFeedPage-CEWktcV8.js → ActivityFeedPage-ILXMZXcb.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-Bnu8aoF2.js → AgentBrowserCapture-UcsupCy7.js} +1 -1
  3. package/client/dist/assets/{AgentModeAnalyticsPane-N0k6nlj0.js → AgentModeAnalyticsPane-CQuxYh8B.js} +2 -2
  4. package/client/dist/assets/{AgentModeCodePane-7s6TnZGe.js → AgentModeCodePane-zPWbW7ei.js} +2 -2
  5. package/client/dist/assets/{AgentModeJobsPane-Btsb5Sq4.js → AgentModeJobsPane-CInByIdl.js} +1 -1
  6. package/client/dist/assets/{AgentsPage-BXgiO5qZ.js → AgentsPage-BWvgxaBv.js} +1 -1
  7. package/client/dist/assets/{AnalyticsPage-CO8-ON4i.js → AnalyticsPage-DP3jakv-.js} +1 -1
  8. package/client/dist/assets/{CodePage-CMSInNsE.js → CodePage-BKXiwE5a.js} +1 -1
  9. package/client/dist/assets/{DesktopAnalyticsPage-1ekD8Gob.js → DesktopAnalyticsPage-DryJOaMz.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-CZ1QztVB.js → DocsDialog-BsCG9ksG.js} +1 -1
  11. package/client/dist/assets/{DocsPage-DHdvuLeh.js → DocsPage-Cc-GeKYl.js} +1 -1
  12. package/client/dist/assets/{InteractiveJobComposer-DDYgxV10.js → InteractiveJobComposer-DfLF6N0f.js} +1 -1
  13. package/client/dist/assets/{JobDetailModal-DSsX48Vu.js → JobDetailModal-DHeur9YK.js} +1 -1
  14. package/client/dist/assets/{JobDetailPage-DbzshFhr.js → JobDetailPage-CraDKzeG.js} +1 -1
  15. package/client/dist/assets/{JobsPage-DY4qRt9_.js → JobsPage-CcbuSwj7.js} +1 -1
  16. package/client/dist/assets/{LoopBuilderPage-DTqzBtuf.js → LoopBuilderPage-BKtNp494.js} +1 -1
  17. package/client/dist/assets/{LoopPreviewModal-C-NbAvIZ.js → LoopPreviewModal-DbZd9isy.js} +1 -1
  18. package/client/dist/assets/{LoopsPage-C-RjiAxo.js → LoopsPage-DEg6sIkG.js} +1 -1
  19. package/client/dist/assets/{PluginsPage-BUdxGBro.js → PluginsPage-BVqSsPzL.js} +1 -1
  20. package/client/dist/assets/{ProjectSettingsDialog-e7y_uZIz.js → ProjectSettingsDialog-CAjLgJAZ.js} +1 -1
  21. package/client/dist/assets/{ReviewPacketPage-Bs_jaLbJ.js → ReviewPacketPage-D-UjTef3.js} +1 -1
  22. package/client/dist/assets/{TemplatePreviewModal-DoeJuvcY.js → TemplatePreviewModal-Bqs-9fhn.js} +1 -1
  23. package/client/dist/assets/{index-Cn-T8bgL.js → index-BfAZg5DG.js} +6 -6
  24. package/client/dist/index.html +1 -1
  25. package/docs/ci-cd.md +4 -0
  26. package/package.json +1 -1
  27. package/server/dist/agent-chat-manager.js +17 -7
  28. package/server/dist/agent-chat-router.js +1 -0
  29. package/server/dist/mobile/mobile-code.js +100 -0
  30. package/server/dist/mobile/mobile-missions.js +1 -1
  31. package/server/dist/mobile/mobile-router.js +12 -6
@@ -204,7 +204,7 @@
204
204
  .specrails-splash__mark .pill { opacity: 1; }
205
205
  }
206
206
  </style>
207
- <script type="module" crossorigin src="/assets/index-Cn-T8bgL.js"></script>
207
+ <script type="module" crossorigin src="/assets/index-BfAZg5DG.js"></script>
208
208
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CNC7AqOf.js">
209
209
  <link rel="modulepreload" crossorigin href="/assets/jsx-runtime-zu2_FqZY.js">
210
210
  <link rel="modulepreload" crossorigin href="/assets/react-dom-DiWzBDP9.js">
package/docs/ci-cd.md CHANGED
@@ -41,6 +41,10 @@ The macOS job retains `artifacts/chromium-signing/` as the `chromium-signing-mac
41
41
 
42
42
  ## Credentials and external requirements
43
43
 
44
+ ### Recover an already published Desktop download channel
45
+
46
+ If native builds and GitHub publication succeed but Hostinger installer uploads fail, run **Recover Desktop download channel** on `main` with the existing stable version (for example `2.54.0`). It requires successful main CI and successful native builds for that exact release commit, then streams all three published installers and verifies their sizes and GitHub SHA-256 digests. It atomically replaces only `latest/manifest.json` over verified FTPS, pointing downloads to the existing version-specific GitHub assets. It refuses version downgrades, verifies the public manifest after publication, and does not rebuild, delete installers, or alter the signed Tauri updater. Normal releases continue to use the regular distribution workflow.
47
+
44
48
  - `RELEASE_PAT` must be authorized for release-please's pull requests and releases. It also permits tag pushes to trigger publication workflows; events created with the repository's ordinary `GITHUB_TOKEN` generally do not trigger another workflow.
45
49
  - Publication uses one of two credentials. When the `NPM_TOKEN` secret is set, it must authenticate an account with publishing permission for `specrails-desktop` (npm now requires such tokens to be granular with *bypass 2FA*, and they expire within 90 days); the workflow checks authentication and gives an explicit error before publishing. When the secret is empty, the job publishes through npm **trusted publishing**: it installs npm 11, removes the empty token line that `setup-node` writes, and lets npm exchange the job's OIDC identity (`id-token: write`) for a short-lived publish grant. That path only works once the package's *Trusted publisher* on npmjs.com names this repository (`fjpulidop/specrails-desktop`) and workflow file (`npm-publish.yml`, no environment); registry configuration is never changed from here. Provenance does **not** grant publishing permission in either mode. An npm `E404` during publication is npm's answer to an unauthorized publish and does not identify which token or permission failed; an `E403` mentioning two-factor authentication means the stored token lacks *bypass 2FA*. [npm publishing and provenance](https://docs.github.com/en/actions/tutorials/publish-packages/publish-nodejs-packages).
46
50
  - Existing Apple signing/notarization and Tauri updater signing secrets remain required. Installer build steps retain their platform-specific checks.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.54.0",
3
+ "version": "2.55.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -194,6 +194,9 @@ class AgentChatManager {
194
194
  })),
195
195
  };
196
196
  }
197
+ notifyConversationCreated(conversationId) {
198
+ this._broadcast({ type: 'agent_conversation_created', conversationId, timestamp: new Date().toISOString() });
199
+ }
197
200
  _startLifecycle(conversationId) {
198
201
  const startedAt = new Date().toISOString();
199
202
  this._turnStartedAt.set(conversationId, startedAt);
@@ -400,13 +403,20 @@ class AgentChatManager {
400
403
  const currentMessageId = options.queueId ? (0, agent_input_store_1.getAgentInput)(this._db, conversationId, options.queueId)?.messageId : null;
401
404
  const historyBlock = (0, agent_context_resolver_1.buildAgentHistoryBlock)((0, agent_input_store_1.decorateAgentInputMessages)(this._db, (0, agent_store_1.listAgentMessages)(this._db, conversationId))
402
405
  .filter((message) => message.id !== currentMessageId && message.delivery_status !== 'cancelled' && message.delivery_status !== 'interrupted'));
403
- if (options.queueId) {
404
- if (!(0, agent_input_store_1.deliverAgentInput)(this._db, conversationId, options.queueId, 'sent'))
405
- return;
406
- }
407
- else {
408
- (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'user', content: userText, attachmentIds, contextRefs: options.contextRefs });
409
- }
406
+ const message = options.queueId
407
+ ? (0, agent_input_store_1.deliverAgentInput)(this._db, conversationId, options.queueId, 'sent')
408
+ : (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'user', content: userText, attachmentIds, contextRefs: options.contextRefs });
409
+ if (!message)
410
+ return;
411
+ // Queued turns were announced by the drain loop. Direct turns must also
412
+ // reach every open client, including Desktop when Companion sent the input.
413
+ if (!currentMessageId)
414
+ this._broadcast({
415
+ type: 'agent_input_started', conversationId, queueId: options.queueId ?? null,
416
+ messageId: message.id, text: message.content, timestamp: message.created_at,
417
+ contextRefs: options.contextRefs ?? [], attachmentIds: attachmentIds ?? [],
418
+ deliveryReceipt: 'sent',
419
+ });
410
420
  this._autoTitle(conversationId, conversation.title);
411
421
  // Providers WITHOUT a --system-prompt flag (codex, gemini) drop opts.systemPrompt
412
422
  // for chat turns, so the attachment prompt-injection note would never reach them —
@@ -199,6 +199,7 @@ function createAgentChatRouter(deps) {
199
199
  // Off-catalog values fall back to null (= no provider-specific override).
200
200
  reasoningEffort: validEffort(provider, effectiveModel, body.reasoningEffort) ?? null,
201
201
  });
202
+ manager.notifyConversationCreated(conversation.id);
202
203
  res.status(201).json({ conversation });
203
204
  });
204
205
  router.get('/conversations/:id', (req, res) => {
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.relativeCodePath = relativeCodePath;
4
+ exports.createMobileCodeRouter = createMobileCodeRouter;
5
+ const express_1 = require("express");
6
+ const mobile_redact_1 = require("./mobile-redact");
7
+ const id = /^[A-Za-z0-9_-]{1,160}$/;
8
+ function relativeCodePath(value) {
9
+ return typeof value === 'string' && value.length > 0 && value.length <= 4096
10
+ && !/^[\\/]|^[A-Za-z]:|[\x00-\x1f\\]/.test(value)
11
+ && !value.split('/').some(part => part === '..' || part === '.' || part === '');
12
+ }
13
+ const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
14
+ /** Project ACL/auth run on the parent router. Desktop remains authoritative for
15
+ * repository membership, realpath confinement, deny-list and gitignore rules.
16
+ * Only this explicit source DTO preserves source text verbatim: applying the
17
+ * generic absolute-path scrubber to code would silently corrupt its contents.
18
+ */
19
+ function createMobileCodeRouter(upstream) {
20
+ const router = (0, express_1.Router)();
21
+ for (const operation of ['tree', 'find', 'file']) {
22
+ router.get(`/projects/:pid/repositories/:rid/code/${operation}`, async (req, res) => {
23
+ const pid = req.params.pid, rid = req.params.rid;
24
+ if (!id.test(pid) || !id.test(rid)) {
25
+ res.status(400).json({ error: 'Invalid parameter' });
26
+ return;
27
+ }
28
+ const query = new URLSearchParams();
29
+ if (operation === 'tree') {
30
+ query.set('filter', 'all');
31
+ query.set('limit', '200');
32
+ if (req.query.cursor !== undefined) {
33
+ if (typeof req.query.cursor !== 'string' || req.query.cursor.length > 4096) {
34
+ res.status(400).json({ error: 'Invalid cursor' });
35
+ return;
36
+ }
37
+ query.set('cursor', req.query.cursor);
38
+ }
39
+ }
40
+ else if (operation === 'find') {
41
+ if (typeof req.query.q !== 'string' || !req.query.q.trim() || req.query.q.length > 256) {
42
+ res.status(400).json({ error: 'Invalid query' });
43
+ return;
44
+ }
45
+ query.set('q', req.query.q);
46
+ query.set('limit', '50');
47
+ }
48
+ else {
49
+ if (!relativeCodePath(req.query.path)) {
50
+ res.status(400).json({ error: 'Invalid relative path' });
51
+ return;
52
+ }
53
+ query.set('path', req.query.path);
54
+ for (const key of ['startLine', 'startColumn']) {
55
+ const value = req.query[key] ?? '1';
56
+ if (typeof value !== 'string' || !/^[1-9]\d{0,8}$/.test(value)) {
57
+ res.status(400).json({ error: 'Invalid range' });
58
+ return;
59
+ }
60
+ query.set(key, value);
61
+ }
62
+ query.set('endLine', String(Number(query.get('startLine')) + 199));
63
+ if (req.query.expectedHash !== undefined) {
64
+ if (typeof req.query.expectedHash !== 'string' || !/^[a-f0-9]{64}$/i.test(req.query.expectedHash)) {
65
+ res.status(400).json({ error: 'Invalid hash' });
66
+ return;
67
+ }
68
+ query.set('expectedHash', req.query.expectedHash);
69
+ }
70
+ }
71
+ try {
72
+ const response = await upstream('GET', `/api/projects/${encodeURIComponent(pid)}/repositories/${encodeURIComponent(rid)}/code/${operation}?${query}`);
73
+ if (response.status < 200 || response.status >= 300) {
74
+ res.status(response.status).json((0, mobile_redact_1.redact)(response.json));
75
+ return;
76
+ }
77
+ const data = record(response.json);
78
+ if (operation === 'file') {
79
+ if (!relativeCodePath(data.path) || data.path !== req.query.path || typeof data.content !== 'string') {
80
+ res.status(502).json({ error: 'Invalid source response' });
81
+ return;
82
+ }
83
+ res.json({ relativePath: data.path, content: data.content, language: data.language,
84
+ fileHash: data.fileHash, startLine: data.startLine, startColumn: data.startColumn,
85
+ endLine: data.endLine, totalLines: data.totalLines, nextLine: data.nextLine, nextColumn: data.nextColumn });
86
+ }
87
+ else {
88
+ const rows = data[operation === 'tree' ? 'entries' : 'matches'];
89
+ res.json({ entries: (Array.isArray(rows) ? rows : []).map(record).filter(row => relativeCodePath(row.path)).map(row => ({
90
+ relativePath: row.path, kind: row.kind === 'dir' ? 'dir' : 'file', sizeBytes: row.sizeBytes,
91
+ })), nextCursor: operation === 'tree' ? data.nextCursor : null, truncated: data.truncated === true });
92
+ }
93
+ }
94
+ catch {
95
+ res.status(502).json({ error: 'Code explorer unavailable' });
96
+ }
97
+ });
98
+ }
99
+ return router;
100
+ }
@@ -18,7 +18,7 @@ exports.MOBILE_PROTOCOL_VERSION = 2;
18
18
  exports.MISSION_ACCESS_REASON = 'Mission agents can access more than their pinned project. Missions require an all-projects device grant; your permitted Specs and Rails remain available.';
19
19
  function mobileCapabilities(db, deviceId) {
20
20
  const missions = process.env.SPECRAILS_AGENT_CHAT !== 'false' && (0, mobile_devices_1.getAllowedProjects)(db, deviceId) === null;
21
- return { protocolVersion: exports.MOBILE_PROTOCOL_VERSION, features: { missions, missionControl: missions, missionQueue: missions, missionProcesses: missions, repositories: true, missionRailCards: missions && (0, feature_flags_1.isMissionRailCardsEnabled)() },
21
+ return { protocolVersion: exports.MOBILE_PROTOCOL_VERSION, features: { missions, missionControl: missions, missionQueue: missions, missionProcesses: missions, repositories: true, codeExplorer: (0, feature_flags_1.isCodeExplorerEnabled)(), missionRailCards: missions && (0, feature_flags_1.isMissionRailCardsEnabled)() },
22
22
  ...(!missions ? { missionsUnavailableReason: process.env.SPECRAILS_AGENT_CHAT === 'false' ? 'Missions are disabled on Desktop.' : exports.MISSION_ACCESS_REASON } : {}) };
23
23
  }
24
24
  function conversationView(value) {
@@ -8,6 +8,7 @@ const mobile_redact_1 = require("./mobile-redact");
8
8
  const mobile_auth_1 = require("./mobile-auth");
9
9
  const mobile_devices_1 = require("./mobile-devices");
10
10
  const mobile_missions_1 = require("./mobile-missions");
11
+ const mobile_code_1 = require("./mobile-code");
11
12
  // The gateway's authenticated REST surface: the `/v1/*` allow-list. Each route
12
13
  // forwards, in-process, via a REAL loopback HTTP request to
13
14
  // http://127.0.0.1:<desktopPort> with the master token injected server-side as
@@ -448,12 +449,14 @@ function createMobileRouter(deps) {
448
449
  narrowed.acceptanceCriteria = b.acceptanceCriteria.filter((x) => typeof x === 'string');
449
450
  void forward(res, 'POST', `/api/projects/${encodeURIComponent(pid)}/tickets/from-draft`, '', narrowed);
450
451
  });
451
- v1.use((0, mobile_missions_1.createMobileMissionsRouter)({ db: deps.db, upstream: async (method, path, body) => {
452
- const response = await fetch(internalBase + path, { method, signal: AbortSignal.timeout(30_000),
453
- headers: { 'x-desktop-token': (0, auth_1.loadOrGenerateToken)(), ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}) },
454
- ...(body !== undefined ? { body: JSON.stringify(body) } : {}) });
455
- return { status: response.status, json: await response.json() };
456
- } }));
452
+ const mobileUpstream = async (method, path, body) => {
453
+ const response = await fetch(internalBase + path, { method, signal: AbortSignal.timeout(30_000),
454
+ headers: { 'x-desktop-token': (0, auth_1.loadOrGenerateToken)(), ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}) },
455
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}) });
456
+ return { status: response.status, json: await response.json() };
457
+ };
458
+ v1.use((0, mobile_missions_1.createMobileMissionsRouter)({ db: deps.db, upstream: mobileUpstream }));
459
+ v1.use((0, mobile_code_1.createMobileCodeRouter)(mobileUpstream));
457
460
  router.use('/v1', v1);
458
461
  // Any unmatched gateway path → 404 JSON (NEVER falls through to a SPA handler;
459
462
  // the gateway serves JSON/WS only, no static client).
@@ -465,6 +468,9 @@ function createMobileRouter(deps) {
465
468
  /** The allow-list, exported for the CI drift test (asserts each entry resolves
466
469
  * against a real internal route, and that no traversal param is accepted). */
467
470
  exports.MOBILE_ALLOWLIST = [
471
+ { method: 'GET', path: '/v1/projects/:pid/repositories/:rid/code/tree' },
472
+ { method: 'GET', path: '/v1/projects/:pid/repositories/:rid/code/find' },
473
+ { method: 'GET', path: '/v1/projects/:pid/repositories/:rid/code/file' },
468
474
  { method: 'GET', path: '/v1/capabilities' },
469
475
  { method: 'GET', path: '/v1/mission-models' },
470
476
  { method: 'PATCH', path: '/v1/projects/:pid/missions/:cid/messages/:mid/intent' },