Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
3f964fa
docs: define store screenshot phase one
sucaifenxiang Jul 28, 2026
c657e8b
docs: translate store screenshot design to Chinese
sucaifenxiang Jul 28, 2026
da306f9
docs: add Chinese store screenshot implementation plan
sucaifenxiang Jul 28, 2026
31563cf
chore: ignore local implementation worktrees
sucaifenxiang Jul 28, 2026
58356d9
feat: add store screenshot domain model
sucaifenxiang Jul 28, 2026
5cbeb64
fix: complete store screenshot platform specs
sucaifenxiang Jul 28, 2026
34ff34f
feat: add deterministic screenshot templates
sucaifenxiang Jul 28, 2026
d5d9819
fix: validate screenshot changesets
sucaifenxiang Jul 28, 2026
73654fa
fix: preserve screenshot changeset locks
sucaifenxiang Jul 28, 2026
7853a09
feat: define store screenshot API contracts
sucaifenxiang Jul 28, 2026
b8d55a0
fix: align store screenshot contracts
sucaifenxiang Jul 28, 2026
1c8fbbc
feat: persist store screenshot documents
sucaifenxiang Jul 28, 2026
7dc7338
fix: make screenshot persistence race safe
sucaifenxiang Jul 28, 2026
be0acbb
feat: expose store screenshot HTTP API
sucaifenxiang Jul 29, 2026
4c6f364
fix: enforce screenshot job ownership
sucaifenxiang Jul 29, 2026
c7a23bd
test: preserve screenshot future dependency types
sucaifenxiang Jul 29, 2026
aa85e60
feat: render and export store screenshots
sucaifenxiang Jul 29, 2026
7888786
fix: render uploaded screenshot assets
sucaifenxiang Jul 29, 2026
b863939
test: close screenshot asset validation gaps
sucaifenxiang Jul 29, 2026
5ab2d84
feat: generate structured screenshot plans
sucaifenxiang Jul 29, 2026
0f62e2c
fix: harden screenshot plan generation
sucaifenxiang Jul 29, 2026
ded8fea
fix: validate screenshot pricing claims
sucaifenxiang Jul 29, 2026
99d80d5
fix: satisfy daemon test types
sucaifenxiang Jul 29, 2026
6bcba22
feat: add store screenshot CLI
sucaifenxiang Jul 29, 2026
e7ddd06
fix: harden store screenshot CLI contracts
sucaifenxiang Jul 29, 2026
f58b61c
feat: add store screenshot workspace
sucaifenxiang Jul 29, 2026
08ed212
fix: harden store screenshot workspace
sucaifenxiang Jul 29, 2026
0e7f67d
fix: make store screenshot recovery resilient
sucaifenxiang Jul 29, 2026
ef3772b
fix: bound store screenshot recovery registry
sucaifenxiang Jul 29, 2026
e03c4d4
feat: add focused screenshot editing
sucaifenxiang Jul 29, 2026
6da2b43
fix: align screenshot editor previews
sucaifenxiang Jul 29, 2026
a6aa7ea
fix: harden screenshot review and assets
sucaifenxiang Jul 29, 2026
45a1543
test: cover store screenshot workflow
sucaifenxiang Jul 29, 2026
ace34a8
test: stabilize store screenshot history visual
sucaifenxiang Jul 29, 2026
82d05cd
test: harden store screenshot e2e
sucaifenxiang Jul 29, 2026
4bfbf0c
test: refresh store screenshot visual fixtures
sucaifenxiang Jul 29, 2026
eec156b
test: pin two-platform screenshot validation
sucaifenxiang Jul 29, 2026
53daf9a
test(daemon): stabilize environment-sensitive baselines
sucaifenxiang Aug 3, 2026
de6cbda
Merge remote-tracking branch 'upstream/main' into feature/store-scree…
sucaifenxiang Aug 5, 2026
71bc02a
docs(store-screenshot): point acceptance evidence at repo-scoped .tmp…
sucaifenxiang Aug 5, 2026
6bb829a
Merge remote-tracking branch 'upstream/main' into feature/store-scree…
sucaifenxiang Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,9 @@ deploy/agent-bins/
# Local agent worktrees (must stay git-ignored so pnpm guard does not scan them)
.slim/worktrees/
.slim/worktrees.json

# Local brainstorming companion state and mockups.
.superpowers/

# Local implementation worktrees.
.worktrees/
5 changes: 4 additions & 1 deletion apps/daemon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"typecheck": "pnpm --filter @open-design/contracts build && pnpm --filter @open-design/registry-protocol build && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit"
},
"dependencies": {
"@launch-studio/store-screenshot": "workspace:*",
"@modelcontextprotocol/sdk": "1.29.0",
"@open-design/agui-adapter": "workspace:*",
"@open-design/contracts": "workspace:*",
Expand All @@ -58,8 +59,10 @@
"posthog-node": "5.34.6",
"pptxgenjs": "4.0.1",
"prom-client": "15.1.3",
"sharp": "0.34.5",
"tar": "7.5.15",
"undici": "7.25.0"
"undici": "7.25.0",
"zod-v3": "npm:zod@3.25.76"
},
"devDependencies": {
"@types/better-sqlite3": "7.6.13",
Expand Down
21 changes: 21 additions & 0 deletions apps/daemon/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { BRAND_USAGE, isBrandHelpArg } from './cli-help/index.js';
import { parseDesignSystemRenameArgs } from './design-systems/rename-args.js';
import { runLiveArtifactsToolCli } from './tools-live-artifacts-cli.js';
import { splitResearchSubcommand } from './research/cli-args.js';
import { runStoreScreenshotCli } from './store-screenshots/cli.js';
import { resolveDaemonUrl } from './daemon-url.js';
import { requestJsonIpc } from '@open-design/sidecar';
import { SIDECAR_ENV, SIDECAR_MESSAGES } from '@open-design/sidecar-proto';
Expand Down Expand Up @@ -408,6 +409,7 @@ const SUBCOMMAND_MAP = {
config: runConfig,
library: runLibrary,
figma: runFigma,
'store-screenshot': runStoreScreenshot,
};

const EXPORT_STRING_FLAGS = new Set([
Expand Down Expand Up @@ -711,6 +713,10 @@ function printRootHelp() {
schedule, trigger, or harvest results from a routine without
opening the web UI.

od store-screenshot <create|upload|generate|validate|export|status|versions|restore> [args]
Create, generate, validate, export, and version store screenshots
through the same daemon API used by the web UI.

od message-center <list|read|read-all> [args]
Read and acknowledge message-center inbox items through the same
daemon endpoints the bell UI uses.
Expand Down Expand Up @@ -1403,6 +1409,21 @@ async function runArtifacts(args) {
process.exit(exitCode);
}

async function runStoreScreenshot(args) {
const { exitCode } = await runStoreScreenshotCli(args, {
onHttpFailure: structuredHttpFailure,
onNetworkFailure: (error, baseUrl) => {
exitWithStructuredError({
code: 'daemon-not-running',
message: `failed to reach daemon at ${baseUrl}: ${
error instanceof Error ? error.message : String(error)
}`,
});
},
});
process.exit(exitCode);
}

function printResearchHelp() {
console.log(`Usage:
od research search --query <text> [--max-sources 5] [--daemon-url <url>]
Expand Down
2 changes: 2 additions & 0 deletions apps/daemon/src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { migrateCritique } from './critique/persistence.js';
import { migrateMediaTasks } from './media/tasks.js';
import { migrateLibrary } from './library-store.js';
import { migratePlugins } from './plugins/persistence.js';
import { migrateStoreScreenshots } from './store-screenshots/persistence.js';

type SqliteDb = Database.Database;
type DbRow = Record<string, any>;
Expand Down Expand Up @@ -542,6 +543,7 @@ function migrate(db: SqliteDb): void {
migrateMediaTasks(db);
migrateLibrary(db);
migratePlugins(db);
migrateStoreScreenshots(db);
migrateCollabSyncSnapshots(db);
migrateCommentRelayOutbox(db);
}
Expand Down
140 changes: 110 additions & 30 deletions apps/daemon/src/memory-llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -949,17 +949,6 @@ function extractJsonEventText(kind, raw, agentName) {
}

async function callLocalCli(provider, system, user, options) {
if (typeof options?.localCliRunner === 'function') {
return options.localCliRunner({
agentId: provider.agentId,
model: provider.model,
system,
user,
projectRoot: options?.projectRoot ?? null,
dataDir: options?.dataDir ?? null,
});
}

const def = getAgentDef(provider.agentId);
if (!def) {
throw new Error(`Local CLI agent "${provider.agentId}" is not installed`);
Expand All @@ -972,6 +961,32 @@ async function callLocalCli(provider, system, user, options) {
} catch {
configuredAgentEnv = {};
}
const configuredSecrets = Object.entries({
...process.env,
...configuredAgentEnv,
})
.filter(([key, value]) => (
/(API_KEY|AUTH_TOKEN|RUNTIME_KEY|ACCESS_TOKEN|SECRET_ACCESS_KEY|SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIALS?|PRIVATE_KEY)$/i.test(key)
&& typeof value === 'string'
&& value.length > 0
))
.map(([, value]) => value);

if (typeof options?.localCliRunner === 'function') {
try {
const text = await options.localCliRunner({
agentId: provider.agentId,
model: provider.model,
system,
user,
projectRoot: options?.projectRoot ?? null,
dataDir: options?.dataDir ?? null,
});
return { text, sensitiveValues: configuredSecrets };
} catch (error) {
throw redactProviderError(error, provider, configuredSecrets);
}
}

const launch = resolveAgentLaunch(def, configuredAgentEnv);
if (!launch?.launchPath) {
Expand All @@ -990,7 +1005,7 @@ async function callLocalCli(provider, system, user, options) {
const prompt = [
system,
'',
'You are running as a background memory extractor. Do not use tools. Return strict JSON only.',
'You are running as a background structured-JSON task. Do not use tools. Return strict JSON only.',
'',
user,
].join('\n');
Expand Down Expand Up @@ -1093,15 +1108,19 @@ async function callLocalCli(provider, system, user, options) {
try {
text = parseStdout(stdout);
} catch (err) {
finish(err);
finish(redactProviderError(err, provider, configuredSecrets));
return;
}
if (text) {
finish(null, text);
finish(null, { text, sensitiveValues: configuredSecrets });
return;
}
}
const detail = (stderr.trim() || stdout.trim() || 'no output').slice(0, 1000);
const rawDetail = (stderr.trim() || stdout.trim() || 'no output').slice(0, 1000);
const detail = configuredSecrets.reduce(
(text, secret) => text.split(secret).join('[REDACTED]'),
rawDetail,
);
const status = signal ? `signal ${signal}` : `exit ${code}`;
finish(new Error(`${def.name} CLI ${status}: ${detail}`));
});
Expand All @@ -1112,6 +1131,72 @@ async function callLocalCli(provider, system, user, options) {
});
}

function redactProviderError(error, provider, sensitiveValues = []) {
const raw = error?.message || String(error);
const apiKey =
typeof provider?.apiKey === 'string' ? provider.apiKey.trim() : '';
const exactValues = [apiKey, ...sensitiveValues].filter(Boolean);
const redacted = exactValues.reduce(
(text, value) => text.split(value).join('[REDACTED]'),
raw,
)
.replace(
/((?:api[_-]?key|auth[_-]?token|access[_-]?token|authorization)\s*[:=]\s*)[^\s,;]+/gi,
'$1[REDACTED]',
)
.replace(/\bBearer\s+\S+/gi, 'Bearer [REDACTED]')
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, '[REDACTED]');
return new Error(redacted);
}

// Shared provider transport used by memory extraction and other daemon-owned
// strict-JSON tasks. Provider selection deliberately reuses pickProvider so
// Local CLI, chat BYOK, env, and media-config precedence cannot drift.
export async function generateConfiguredJsonTextWithMetadata(request, options = {}) {
const projectRoot = options?.projectRoot ?? null;
const dataDir = options?.dataDir ?? null;
const provider = options?.provider ?? await pickProvider(
projectRoot,
options?.useMemoryConfig === true ? dataDir : null,
request?.chatAgentId ?? options?.chatAgentId ?? null,
options?.chatProvider ?? null,
options?.chatModel ?? null,
);
if (!provider) return null;
try {
if (provider.transport === 'chat-cli') {
return await callLocalCli(provider, request.system, request.user, {
dataDir,
projectRoot,
localCliRunner: options?.localCliRunner,
});
}
let text;
if (provider.kind === 'anthropic') {
text = await callAnthropic(provider, request.system, request.user);
} else if (provider.kind === 'azure') {
text = await callAzure(provider, request.system, request.user);
} else if (provider.kind === 'google') {
text = await callGoogle(provider, request.system, request.user);
} else {
text = await callOpenAI(provider, request.system, request.user);
}
return {
text,
sensitiveValues: typeof provider.apiKey === 'string' && provider.apiKey
? [provider.apiKey]
: [],
};
} catch (error) {
throw redactProviderError(error, provider);
}
}

export async function generateConfiguredJsonText(request, options = {}) {
const result = await generateConfiguredJsonTextWithMetadata(request, options);
return result?.text ?? null;
}

// Tolerant JSON parse — the model occasionally wraps output in ```json
// fences even when told not to. Strip those defensively.
function parseEntries(rawText) {
Expand Down Expand Up @@ -1312,24 +1397,19 @@ async function collectProposedEntries(dataDir, input, options) {

let raw = '';
try {
if (provider.transport === 'chat-cli') {
raw = await callLocalCli(provider, systemPrompt, userPayload, {
raw = await generateConfiguredJsonText(
{
system: systemPrompt,
user: userPayload,
chatAgentId,
},
{
provider,
dataDir,
projectRoot,
localCliRunner: options?.localCliRunner,
});
} else if (provider.kind === 'anthropic') {
raw = await callAnthropic(provider, systemPrompt, userPayload);
} else if (provider.kind === 'azure') {
raw = await callAzure(provider, systemPrompt, userPayload);
} else if (provider.kind === 'google') {
raw = await callGoogle(provider, systemPrompt, userPayload);
} else {
// openai or ollama — both speak the OpenAI chat-completions
// wire shape, so callOpenAI handles them with just a different
// base URL.
raw = await callOpenAI(provider, systemPrompt, userPayload);
}
},
);
} catch (err) {
// err.message is already pre-formatted by describeFetchError() when
// the call layer caught a network error. For HTTP-level failures
Expand Down
2 changes: 2 additions & 0 deletions apps/daemon/src/route-context-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { RegisterRoutineRoutesDeps } from './routes/routine.js';
import type { RegisterRunRoutesDeps } from './routes/runs.js';
import type { RegisterSocialShareRoutesDeps } from './routes/social-share.js';
import type { RegisterStaticResourceRoutesDeps } from './routes/static-resource.js';
import type { RegisterStoreScreenshotRoutesDeps } from './routes/store-screenshots.js';
import type { RegisterVelaRoutesDeps } from './routes/vela.js';
import type { RegisterXaiRoutesDeps } from './routes/xai.js';

Expand Down Expand Up @@ -45,6 +46,7 @@ type AllRegisteredRouteDeps =
& RegisterRunRoutesDeps
& RegisterSocialShareRoutesDeps
& RegisterStaticResourceRoutesDeps
& RegisterStoreScreenshotRoutesDeps
& RegisterVelaRoutesDeps
& RegisterXaiRoutesDeps;

Expand Down
Loading
Loading