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.
- package/client/dist/assets/{ActivityFeedPage-CEWktcV8.js → ActivityFeedPage-ILXMZXcb.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-Bnu8aoF2.js → AgentBrowserCapture-UcsupCy7.js} +1 -1
- package/client/dist/assets/{AgentModeAnalyticsPane-N0k6nlj0.js → AgentModeAnalyticsPane-CQuxYh8B.js} +2 -2
- package/client/dist/assets/{AgentModeCodePane-7s6TnZGe.js → AgentModeCodePane-zPWbW7ei.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-Btsb5Sq4.js → AgentModeJobsPane-CInByIdl.js} +1 -1
- package/client/dist/assets/{AgentsPage-BXgiO5qZ.js → AgentsPage-BWvgxaBv.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-CO8-ON4i.js → AnalyticsPage-DP3jakv-.js} +1 -1
- package/client/dist/assets/{CodePage-CMSInNsE.js → CodePage-BKXiwE5a.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-1ekD8Gob.js → DesktopAnalyticsPage-DryJOaMz.js} +1 -1
- package/client/dist/assets/{DocsDialog-CZ1QztVB.js → DocsDialog-BsCG9ksG.js} +1 -1
- package/client/dist/assets/{DocsPage-DHdvuLeh.js → DocsPage-Cc-GeKYl.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-DDYgxV10.js → InteractiveJobComposer-DfLF6N0f.js} +1 -1
- package/client/dist/assets/{JobDetailModal-DSsX48Vu.js → JobDetailModal-DHeur9YK.js} +1 -1
- package/client/dist/assets/{JobDetailPage-DbzshFhr.js → JobDetailPage-CraDKzeG.js} +1 -1
- package/client/dist/assets/{JobsPage-DY4qRt9_.js → JobsPage-CcbuSwj7.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-DTqzBtuf.js → LoopBuilderPage-BKtNp494.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-C-NbAvIZ.js → LoopPreviewModal-DbZd9isy.js} +1 -1
- package/client/dist/assets/{LoopsPage-C-RjiAxo.js → LoopsPage-DEg6sIkG.js} +1 -1
- package/client/dist/assets/{PluginsPage-BUdxGBro.js → PluginsPage-BVqSsPzL.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-e7y_uZIz.js → ProjectSettingsDialog-CAjLgJAZ.js} +1 -1
- package/client/dist/assets/{ReviewPacketPage-Bs_jaLbJ.js → ReviewPacketPage-D-UjTef3.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-DoeJuvcY.js → TemplatePreviewModal-Bqs-9fhn.js} +1 -1
- package/client/dist/assets/{index-Cn-T8bgL.js → index-BfAZg5DG.js} +6 -6
- package/client/dist/index.html +1 -1
- package/docs/ci-cd.md +4 -0
- package/package.json +1 -1
- package/server/dist/agent-chat-manager.js +17 -7
- package/server/dist/agent-chat-router.js +1 -0
- package/server/dist/mobile/mobile-code.js +100 -0
- package/server/dist/mobile/mobile-missions.js +1 -1
- package/server/dist/mobile/mobile-router.js +12 -6
package/client/dist/index.html
CHANGED
|
@@ -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-
|
|
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
|
@@ -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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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' },
|