@awak-app/simy-cli 0.7.23 → 0.7.25
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/package.json +1 -1
- package/src/agent.js +16 -1
- package/src/auto-update.js +3 -2
- package/src/browser-handoff.js +14 -3
- package/src/cli-contract.js +1 -1
- package/src/connection-overview.js +58 -0
- package/src/desktop-status.js +9 -0
- package/src/sqm/bundle-store.js +30 -2
- package/src/sqm/provider-plugin-status.js +21 -3
- package/src/web-origin.js +7 -0
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectionOverview, readSqmConnectionObservation } from './connection-overview.js';
|
|
1
2
|
import { readLocalPhotoSource } from "./local-photos.js";
|
|
2
3
|
import { resolveLocalTaskDestination, saveLocalTaskDestination } from "./local-task-destination.js";
|
|
3
4
|
import { observePairedNativeIdentities } from './native-provider-identity.js';
|
|
@@ -1217,7 +1218,7 @@ export async function startAgent({
|
|
|
1217
1218
|
|
|
1218
1219
|
if (req.method === "GET" && url.pathname === "/v1/auth/browser") {
|
|
1219
1220
|
if (req.headers.host !== `127.0.0.1:${port}`) { json(res, 403, { error: "Invalid local host." }); return; }
|
|
1220
|
-
const page = browserHandoffPage();
|
|
1221
|
+
const page = browserHandoffPage(apiOrigin);
|
|
1221
1222
|
res.writeHead(200, page.headers).end(page.html);
|
|
1222
1223
|
return;
|
|
1223
1224
|
}
|
|
@@ -1516,6 +1517,7 @@ export async function startAgent({
|
|
|
1516
1517
|
const previousSession = session ?? reclaimableSession;
|
|
1517
1518
|
const authorizedSession = {
|
|
1518
1519
|
token: String(body.token || ""),
|
|
1520
|
+
email: typeof body.email === "string" && body.email.length <= 254 ? body.email : null,
|
|
1519
1521
|
device_id: typeof body.device_id === "string" ? body.device_id : null,
|
|
1520
1522
|
api_origin: apiOrigin,
|
|
1521
1523
|
api_base_url: apiBaseUrl,
|
|
@@ -2857,8 +2859,21 @@ export async function startAgent({
|
|
|
2857
2859
|
const { codex, claude } = deferStartupSynchronization
|
|
2858
2860
|
? providerStatuses
|
|
2859
2861
|
: await readProviderStatuses();
|
|
2862
|
+
const overviewSession = session;
|
|
2863
|
+
const overviewAccess = desktopAccess.snapshot();
|
|
2864
|
+
let overviewSqm = { state: 'unknown' };
|
|
2865
|
+
if (auth.state === 'signed_in' && !pendingOrganization) {
|
|
2866
|
+
try { overviewSqm = await readSqmConnectionObservation({ endpoint: process.env.SIMY_SQM_BUNDLE_URL?.trim() || new URL('sqm/knowledge-bundle', resolveWebApiBaseUrl(apiOrigin, overviewSession?.api_base_url)).href,
|
|
2867
|
+
accountId: overviewSession?.account_id || overviewSession?.auth_user_id || overviewSession?.device_id,
|
|
2868
|
+
organizationId: overviewSession?.organization_id || overviewSession?.org_id }, { root: process.env.SIMY_SQM_CACHE_DIR || join(sessionRoot, 'sqm') }); } catch { /* A missing endpoint must not block the status frame. */ }
|
|
2869
|
+
}
|
|
2870
|
+
// An organization switch/sign-out while reading local metadata must not
|
|
2871
|
+
// publish an old tenant's identity or SQM result in a fresh frame.
|
|
2872
|
+
const overview = connectionOverview({ webOrigin: apiOrigin, session, access: session === overviewSession ? overviewAccess : null,
|
|
2873
|
+
signedIn: session === overviewSession && auth.state === 'signed_in', switching: Boolean(pendingOrganization), sqm: overviewSqm });
|
|
2860
2874
|
return {
|
|
2861
2875
|
schemaVersion: 1,
|
|
2876
|
+
connectionOverview: overview,
|
|
2862
2877
|
sampledAt: new Date().toISOString(),
|
|
2863
2878
|
version: CLI_VERSION,
|
|
2864
2879
|
webOrigin: apiOrigin,
|
package/src/auto-update.js
CHANGED
|
@@ -15,6 +15,7 @@ import path from "node:path";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
17
|
|
|
18
|
+
import { daemonRestartCommand } from "./web-origin.js";
|
|
18
19
|
import { openExternalUrl } from "./browser.js";
|
|
19
20
|
import {
|
|
20
21
|
installMacosInstallerRelease,
|
|
@@ -1094,7 +1095,7 @@ export function autoUpdateHelpText({ installMode = null, webOrigin = null } = {}
|
|
|
1094
1095
|
return [
|
|
1095
1096
|
"Recommended installation (enables verified CLI auto-update):",
|
|
1096
1097
|
` npm install -g ${CLI_PACKAGE_NAME}`,
|
|
1097
|
-
|
|
1098
|
+
` ${daemonRestartCommand(webOrigin)}`,
|
|
1098
1099
|
].join("\n");
|
|
1099
1100
|
}
|
|
1100
1101
|
|
|
@@ -1131,7 +1132,7 @@ export function updateAction(version, { installMode = "global_npm", webOrigin }
|
|
|
1131
1132
|
return {
|
|
1132
1133
|
id: "install_global_cli",
|
|
1133
1134
|
label: "Install and restart SIMY",
|
|
1134
|
-
command: `npm install -g ${CLI_PACKAGE_NAME}@${version} &&
|
|
1135
|
+
command: `npm install -g ${CLI_PACKAGE_NAME}@${version} && ${daemonRestartCommand(webOrigin)}`,
|
|
1135
1136
|
};
|
|
1136
1137
|
}
|
|
1137
1138
|
|
package/src/browser-handoff.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeWebOrigin } from "./web-origin.js";
|
|
1
2
|
import { createDecipheriv, createHash } from "node:crypto";
|
|
2
3
|
|
|
3
4
|
export const BROWSER_HANDOFF_TRANSPORT = "loopback_redirect_v1";
|
|
@@ -33,6 +34,14 @@ const copy = ja ? {
|
|
|
33
34
|
};
|
|
34
35
|
document.documentElement.lang = ja ? 'ja' : 'en';
|
|
35
36
|
const title = document.getElementById('title'), message = document.getElementById('message');
|
|
37
|
+
const environment = document.getElementById('environment');
|
|
38
|
+
const origin = environment.dataset.origin;
|
|
39
|
+
const host = new URL(origin).hostname;
|
|
40
|
+
const label = origin === 'https://app.simy.one' ? (ja ? '本番' : 'Production')
|
|
41
|
+
: origin === 'https://app-dev.simy.one' ? (ja ? '開発' : 'Development')
|
|
42
|
+
: ['localhost', '127.0.0.1', '[::1]'].includes(host) ? (ja ? 'ローカル開発' : 'Local development')
|
|
43
|
+
: (ja ? 'カスタムホスト' : 'Custom host');
|
|
44
|
+
environment.textContent = (ja ? '接続先環境' : 'Connection environment') + ': ' + label + ' · ' + origin;
|
|
36
45
|
title.textContent = copy.title; message.textContent = copy.waiting;
|
|
37
46
|
const handoff = new URLSearchParams(location.hash.slice(1)).get('handoff');
|
|
38
47
|
history.replaceState(null, '', location.pathname);
|
|
@@ -54,15 +63,17 @@ history.replaceState(null, '', location.pathname);
|
|
|
54
63
|
|
|
55
64
|
// A self-contained local page: credentials never enter the URL query, HTTP
|
|
56
65
|
// request, history or third-party resources. Only the encrypted fragment does.
|
|
57
|
-
const style = `:root{color-scheme:light dark;--bg:#ffffff;--card:#f6f5f4;--ink:#0d0d0d;--muted:#615d59;--line:#e6e6e6;--accent:#0075de} @media(prefers-color-scheme:dark){:root{--bg:#1a1918;--card:#252320;--ink:#f5f3f0;--muted:#b8b3ad;--line:#31302f;--accent:#3b9eff}}*{box-sizing:border-box}body{margin:0;min-height:100dvh;display:grid;place-items:center;padding:24px;background:var(--bg);color:var(--ink);font:16px/1.5 system-ui,sans-serif}main{max-width:480px;width:100%;padding:32px;border:1px solid var(--line);border-radius:12px;background:var(--card)}.brand{color:var(--accent);font-weight:600;font-size:14px}h1{font-size:24px;font-weight:600;margin:16px 0}p{color:var(--muted);margin:0}body[data-state=failed] h1{color:var(--ink)}`;
|
|
66
|
+
const style = `:root{color-scheme:light dark;--bg:#ffffff;--card:#f6f5f4;--ink:#0d0d0d;--muted:#615d59;--line:#e6e6e6;--accent:#0075de} @media(prefers-color-scheme:dark){:root{--bg:#1a1918;--card:#252320;--ink:#f5f3f0;--muted:#b8b3ad;--line:#31302f;--accent:#3b9eff}}*{box-sizing:border-box}body{margin:0;min-height:100dvh;display:grid;place-items:center;padding:24px;background:var(--bg);color:var(--ink);font:16px/1.5 system-ui,sans-serif}main{max-width:480px;width:100%;padding:32px;border:1px solid var(--line);border-radius:12px;background:var(--card)}.brand{color:var(--accent);font-weight:600;font-size:14px}h1{font-size:24px;font-weight:600;margin:16px 0}p{color:var(--muted);margin:0}#environment{margin-top:16px;font-size:14px;overflow-wrap:anywhere}body[data-state=failed] h1{color:var(--ink)}`;
|
|
58
67
|
const hash = value => createHash("sha256").update(value).digest("base64");
|
|
59
|
-
export function browserHandoffPage() {
|
|
68
|
+
export function browserHandoffPage(webOrigin) {
|
|
69
|
+
const origin = normalizeWebOrigin(webOrigin);
|
|
70
|
+
const escapedOrigin = origin.replace(/[&<>"']/g, value => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[value]);
|
|
60
71
|
return {
|
|
61
72
|
headers: {
|
|
62
73
|
"Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store",
|
|
63
74
|
"Referrer-Policy": "no-referrer", "X-Content-Type-Options": "nosniff",
|
|
64
75
|
"Content-Security-Policy": `default-src 'none'; script-src 'sha256-${hash(script)}'; style-src 'sha256-${hash(style)}'; connect-src 'self'; img-src data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'`,
|
|
65
76
|
},
|
|
66
|
-
html: `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SIMY connection</title><link rel="icon" href="data:,"><style>${style}</style></head><body><main aria-live="polite" aria-atomic="true"><div class="brand">SIMY</div><h1 id="title">Connecting to SIMY</h1><p id="message">Keep SIMY open while connecting.</p><noscript>JavaScript is required. Return to SIMY and reconnect using a browser with JavaScript enabled.</noscript></main><script>${script}</script></body></html>`,
|
|
77
|
+
html: `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SIMY connection</title><link rel="icon" href="data:,"><style>${style}</style></head><body><main aria-live="polite" aria-atomic="true"><div class="brand">SIMY</div><h1 id="title">Connecting to SIMY</h1><p id="message">Keep SIMY open while connecting.</p><p id="environment" data-origin="${escapedOrigin}">Connection environment: ${escapedOrigin}</p><noscript>JavaScript is required. Return to SIMY and reconnect using a browser with JavaScript enabled.</noscript></main><script>${script}</script></body></html>`,
|
|
67
78
|
};
|
|
68
79
|
}
|
package/src/cli-contract.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./durable-local-task-steps-contract.js";
|
|
15
15
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
16
16
|
|
|
17
|
-
export const CLI_VERSION = "0.7.
|
|
17
|
+
export const CLI_VERSION = "0.7.25";
|
|
18
18
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
19
19
|
|
|
20
20
|
export function narrativeImageCapabilityMetadata() {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { defaultSessionRoot } from './session-store.js';
|
|
6
|
+
|
|
7
|
+
const safeText = (value, limit = 160) => typeof value === 'string' ? value.replace(/[\x00-\x1f\x7f]/g, '').slice(0, limit) : null;
|
|
8
|
+
const scopeKey = ({ endpoint, accountId, organizationId }) => {
|
|
9
|
+
if (!endpoint || !accountId || !organizationId) return null;
|
|
10
|
+
try { const url = new URL(endpoint); url.username = ''; url.password = ''; url.search = ''; url.hash = '';
|
|
11
|
+
return createHash('sha256').update(JSON.stringify([url.href, accountId, organizationId])).digest('hex');
|
|
12
|
+
} catch { return null; }
|
|
13
|
+
};
|
|
14
|
+
const observationPath = (scope, root) => { const key = scopeKey(scope); return key ? join(root, 'connection-observations', `${key}.json`) : null; };
|
|
15
|
+
|
|
16
|
+
// Only metadata of an actual online bundle lookup; never credentials, rules,
|
|
17
|
+
// executable checks or inferred cross-organization registrations.
|
|
18
|
+
export async function recordSqmConnectionObservation(scope, result, { root = process.env.SIMY_SQM_CACHE_DIR || join(defaultSessionRoot(), 'sqm'), now = Date.now() } = {}) {
|
|
19
|
+
const target = observationPath(scope, root);
|
|
20
|
+
if (!target || !['cloud', 'cache_revalidated', 'unavailable'].includes(result?.source)) return;
|
|
21
|
+
const bundle = result.bundle;
|
|
22
|
+
const verified = bundle?.organization_id === scope.organizationId && bundle?.system_scope?.organization_id === scope.organizationId;
|
|
23
|
+
const repository = safeText(scope.repository, 200);
|
|
24
|
+
if (!repository || !/^[^/\s]+\/[^/\s]+$/.test(repository)) return;
|
|
25
|
+
const value = { version: 1, state: verified ? 'verified' : 'unavailable', repository,
|
|
26
|
+
checkedAt: new Date(now).toISOString(), expiresAt: verified ? bundle.expires_at : null };
|
|
27
|
+
const temp = `${target}.${randomUUID()}.tmp`;
|
|
28
|
+
try { await mkdir(join(root, 'connection-observations'), { recursive: true, mode: 0o700 });
|
|
29
|
+
await writeFile(temp, JSON.stringify(value), { mode: 0o600 }); await rename(temp, target);
|
|
30
|
+
} finally { await rm(temp, { force: true }); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function readSqmConnectionObservation(scope, { root = process.env.SIMY_SQM_CACHE_DIR || join(defaultSessionRoot(), 'sqm'), now = Date.now() } = {}) {
|
|
34
|
+
const target = observationPath(scope, root);
|
|
35
|
+
if (!target) return { state: 'unknown' };
|
|
36
|
+
try {
|
|
37
|
+
const raw = await readFile(target, 'utf8'); if (raw.length > 4096) return { state: 'unknown' };
|
|
38
|
+
const v = JSON.parse(raw), at = Date.parse(v.checkedAt);
|
|
39
|
+
if (v.version !== 1 || !['verified', 'unavailable'].includes(v.state) || !Number.isFinite(at) || at > now ||
|
|
40
|
+
typeof v.repository !== 'string' || !/^[^/\s]+\/[^/\s]+$/.test(v.repository) || v.repository.length > 200) return { state: 'unknown' };
|
|
41
|
+
const current = now - at <= 5 * 60 * 1000 && (v.state !== 'verified' || Date.parse(v.expiresAt) > now);
|
|
42
|
+
return { state: current ? v.state : 'previous', repository: safeText(v.repository, 200), checkedAt: v.checkedAt };
|
|
43
|
+
} catch { return { state: 'unknown' }; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function connectionEnvironment(origin) {
|
|
47
|
+
try { const url = new URL(origin); if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== '/') return { kind: 'unknown', origin: null };
|
|
48
|
+
return { kind: url.origin === 'https://app.simy.one' ? 'production' : url.origin === 'https://app-dev.simy.one' ? 'development' : 'custom', origin: url.origin };
|
|
49
|
+
} catch { return { kind: 'unknown', origin: null }; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function connectionOverview({ webOrigin, session, access, signedIn, switching, sqm, deviceName = hostname() }) {
|
|
53
|
+
const scopeReady = signedIn && !switching && session?.organization_id && access?.organizationId === session.organization_id && access?.state === 'ready';
|
|
54
|
+
return { device: { name: safeText(deviceName) || null }, environment: connectionEnvironment(webOrigin),
|
|
55
|
+
organization: { name: signedIn && access?.organizationId === session?.organization_id ? safeText(access?.organizationName) : null,
|
|
56
|
+
state: switching ? 'switching' : scopeReady ? 'ready' : 'unknown' },
|
|
57
|
+
sqm: scopeReady ? (sqm || { state: 'unknown' }) : { state: 'unknown' } };
|
|
58
|
+
}
|
package/src/desktop-status.js
CHANGED
|
@@ -67,6 +67,7 @@ export function desktopAuthStatus(session, { now = Date.now(), webOrigin = null
|
|
|
67
67
|
state: valid ? "signed_in" : exists ? "expired" : "signed_out",
|
|
68
68
|
expiresAt: typeof session?.expires_at === "string" ? session.expires_at : null,
|
|
69
69
|
userLabel: sessionUserLabel(session),
|
|
70
|
+
userEmail: valid ? sessionUserEmail(session) : null,
|
|
70
71
|
userId: valid ? (session.account_id ?? session.auth_user_id ?? null) : null,
|
|
71
72
|
};
|
|
72
73
|
}
|
|
@@ -323,6 +324,14 @@ function reportPresentation(result, allowSchemaLess = false) {
|
|
|
323
324
|
} catch { return null; }
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
function sessionUserEmail(session) {
|
|
328
|
+
for (const key of ["email", "user_label"]) {
|
|
329
|
+
const value = session?.[key];
|
|
330
|
+
if (typeof value === "string" && value.trim().length <= 254 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim())) return value.trim();
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
326
335
|
function sessionUserLabel(session) {
|
|
327
336
|
if (!session) return null;
|
|
328
337
|
for (const key of ["user_label", "display_name", "email", "account_id", "auth_user_id"]) {
|
package/src/sqm/bundle-store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recordSqmConnectionObservation } from '../connection-overview.js';
|
|
1
2
|
import { ADVISORY_GUIDANCE_CAPABILITY, isAdvisoryGuidance } from './advisory-guidance.js';
|
|
2
3
|
import { safeSqmDiagnostic } from "./diagnostics.js";
|
|
3
4
|
import { createHash, createPublicKey } from "node:crypto";
|
|
@@ -25,13 +26,40 @@ export class SqmIncompatibleCliError extends Error {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export async function loadKnowledgeBundle({
|
|
28
|
-
repository,
|
|
29
|
-
cliVersion,
|
|
30
29
|
endpoint = process.env.SIMY_SQM_BUNDLE_URL,
|
|
31
30
|
token = process.env.SIMY_SQM_TOKEN || process.env.SIMY_ACCESS_TOKEN,
|
|
32
31
|
accountId = process.env.SIMY_SQM_ACCOUNT_ID || (token ? `token:${createHash("sha256").update(token).digest("hex")}` : "anonymous"),
|
|
33
32
|
organizationId = process.env.SIMY_SQM_ORGANIZATION_ID,
|
|
34
33
|
cacheRoot = process.env.SIMY_SQM_CACHE_DIR || path.join(process.env.SIMY_HOME?.trim() || path.join(homedir(), ".simy"), "sqm"),
|
|
34
|
+
...options
|
|
35
|
+
}) {
|
|
36
|
+
const context = { ...options, endpoint, token, accountId, organizationId, cacheRoot };
|
|
37
|
+
const observe = async result => {
|
|
38
|
+
if (options.offline || !endpoint || !accountId || !organizationId) return;
|
|
39
|
+
try { await recordSqmConnectionObservation(context, result, { root: cacheRoot, now: options.now }); }
|
|
40
|
+
catch { /* Display metadata must not change SQM behavior. */ }
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
const result = await loadKnowledgeBundleImpl(context);
|
|
44
|
+
// An online fallback retains usable knowledge, but does not confirm the
|
|
45
|
+
// current connection. Offline/cache-only reads never refresh this record.
|
|
46
|
+
await observe(result.source === 'stale_cache' ? { source: 'unavailable' } : result);
|
|
47
|
+
return result;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// Cancellation is an intentional stop, not a failed connection attempt.
|
|
50
|
+
if (!options.signal?.aborted) await observe({ source: 'unavailable' });
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function loadKnowledgeBundleImpl({
|
|
56
|
+
repository,
|
|
57
|
+
cliVersion,
|
|
58
|
+
endpoint,
|
|
59
|
+
token,
|
|
60
|
+
accountId,
|
|
61
|
+
organizationId,
|
|
62
|
+
cacheRoot,
|
|
35
63
|
ttlMs = numberEnv("SIMY_SQM_CACHE_TTL_MS", DEFAULT_TTL_MS),
|
|
36
64
|
offline = false,
|
|
37
65
|
refresh = false,
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { resolveBackendExecutable } from '../backend-executable.js';
|
|
4
5
|
import { runProviderCommand } from './provider-command.js';
|
|
5
6
|
import { CLAUDE_PLUGIN_SELECTOR } from './claude-plugin.js';
|
|
6
7
|
|
|
8
|
+
// The managed marketplace selects the inventory scope, never installation or
|
|
9
|
+
// enablement evidence. Those still come from the provider's actual response.
|
|
10
|
+
async function managedCodexMarketplace({ home, signal }) {
|
|
11
|
+
try {
|
|
12
|
+
const bytes = await readFile(join(home, '.agents', 'plugins', 'marketplace.json'), { encoding: 'utf8', signal });
|
|
13
|
+
if (Buffer.byteLength(bytes) > 64 * 1024) return null;
|
|
14
|
+
const manifest = JSON.parse(bytes);
|
|
15
|
+
return /^[A-Za-z0-9_-]+$/.test(manifest?.name || '') && Array.isArray(manifest.plugins) &&
|
|
16
|
+
manifest.plugins.some(entry => entry?.name === 'simy') ? manifest.name : null;
|
|
17
|
+
} catch { return null; }
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export const unavailablePluginStatus = () => ({ state: 'unknown', installed: null, enabled: null });
|
|
8
21
|
|
|
9
22
|
// Provider inventories describe the actual installation and enablement. Managed
|
|
@@ -15,7 +28,7 @@ export async function inspectProviderPlugins({ home = homedir(),
|
|
|
15
28
|
claudeHome = process.env.CLAUDE_CONFIG_DIR || join(home, '.claude'),
|
|
16
29
|
resolveExecutable = resolveBackendExecutable, run = runProviderCommand,
|
|
17
30
|
timeoutMs = 5_000, signal, providers = ['codex', 'claude'], details = false,
|
|
18
|
-
onDiagnostic = () => {},
|
|
31
|
+
onDiagnostic = () => {}, readCodexMarketplace = managedCodexMarketplace,
|
|
19
32
|
} = {}) {
|
|
20
33
|
if (providers.some(provider => !['codex', 'claude'].includes(provider))) throw new Error('Unsupported provider.');
|
|
21
34
|
const inspect = async provider => {
|
|
@@ -30,7 +43,12 @@ export async function inspectProviderPlugins({ home = homedir(),
|
|
|
30
43
|
const executable = await resolveExecutable(provider, { signal, timeoutMs });
|
|
31
44
|
if (signal?.aborted) return unavailable('cancelled');
|
|
32
45
|
if (!executable) return unavailable('cli_missing');
|
|
33
|
-
const
|
|
46
|
+
const marketplace = provider === 'codex' ? await readCodexMarketplace({ home, signal }) : null;
|
|
47
|
+
if (signal?.aborted) return unavailable('cancelled');
|
|
48
|
+
// A global inventory may contact unrelated remote marketplaces. Query the
|
|
49
|
+
// configured SIMY source so their network delays cannot hide local state.
|
|
50
|
+
const args = ['plugin', 'list', '--json', ...(marketplace ? ['--marketplace', marketplace] : [])];
|
|
51
|
+
const { stdout } = await run(executable, args, {
|
|
34
52
|
timeout: timeoutMs, maxBuffer: 1024 * 1024, signal, cwd: home,
|
|
35
53
|
env: { ...process.env, HOME: home,
|
|
36
54
|
...(provider === 'codex' ? { CODEX_HOME: codexHome } : { CLAUDE_CONFIG_DIR: claudeHome }) },
|
|
@@ -39,7 +57,7 @@ export async function inspectProviderPlugins({ home = homedir(),
|
|
|
39
57
|
const entries = provider === 'codex' ? payload?.installed : payload;
|
|
40
58
|
if (!Array.isArray(entries)) return unavailable('invalid_inventory');
|
|
41
59
|
const matches = entries.filter(entry => provider === 'codex'
|
|
42
|
-
? typeof entry?.pluginId === 'string' && /^simy@[A-Za-z0-9_-]+$/.test(entry.pluginId)
|
|
60
|
+
? typeof entry?.pluginId === 'string' && /^simy@[A-Za-z0-9_-]+$/.test(entry.pluginId) && (!marketplace || entry.pluginId === `simy@${marketplace}`)
|
|
43
61
|
: entry?.id === CLAUDE_PLUGIN_SELECTOR);
|
|
44
62
|
// Prefer the account installation; report a project-only install explicitly
|
|
45
63
|
// rather than silently treating it as an installation for every project.
|
package/src/web-origin.js
CHANGED
|
@@ -44,3 +44,10 @@ export function isRequestOriginAllowed(requestOrigin, webOrigin) {
|
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
// Explicitly retain the resolved host when a command is copied to a fresh shell.
|
|
49
|
+
export function daemonRestartCommand(webOrigin) {
|
|
50
|
+
const origin = resolveWebOrigin(webOrigin);
|
|
51
|
+
const quotedOrigin = '"' + origin.replace(/["\\$`]/g, "\\$&") + '"';
|
|
52
|
+
return `simy --daemon --host ${quotedOrigin}`;
|
|
53
|
+
}
|