@zoahhq/cli 0.1.2 → 0.1.3
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/README.md +10 -10
- package/dist/cli.mjs +181 -175
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ fresh `agent/<timestamp>` branch off main, so an agent's edits stay reviewable
|
|
|
139
139
|
rather than landing on main.
|
|
140
140
|
|
|
141
141
|
**4. Check it worked.** `status` reports the connected project, the branch, and
|
|
142
|
-
|
|
142
|
+
organization role. It also reports credential health. `whoami` reports the
|
|
143
143
|
identity, role, and instance.
|
|
144
144
|
|
|
145
145
|
#### What the agent gets
|
|
@@ -149,17 +149,17 @@ session without direct human confirmation. Confirmation actions stay in the
|
|
|
149
149
|
Zoah app. The server replaces renderer-only and workspace-agent verbs with
|
|
150
150
|
the session tools below.
|
|
151
151
|
|
|
152
|
-
| Group
|
|
153
|
-
|
|
|
154
|
-
| Session
|
|
155
|
-
|
|
|
156
|
-
| Branches
|
|
157
|
-
| Pull requests
|
|
158
|
-
| Canvas
|
|
152
|
+
| Group | Tools |
|
|
153
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
154
|
+
| Session | `connect`, `status`, `pullEvents`, `disconnect`, `whoami`, `logout`, `describeProperties` |
|
|
155
|
+
| Organizations and projects | `listOrganizations`, `createOrganization`, `listProjects`, `createProject`, `updateProject` |
|
|
156
|
+
| Branches | `listBranches`, `createBranch` |
|
|
157
|
+
| Pull requests | `createPullRequest`, `getPullRequest`, `updatePullRequest`, `commentOnPullRequest` |
|
|
158
|
+
| Canvas | one per applicable Action API verb for nodes, pages, components, slots, variants, variables, themes, breakpoints, events, and queries |
|
|
159
159
|
|
|
160
160
|
Two properties are worth knowing. Mutation tools return only after the sync
|
|
161
161
|
gateway acks the write, so a returned success means persisted, not queued. And
|
|
162
|
-
the server does not provide branch, project, or
|
|
162
|
+
the server does not provide branch, project, or organization deletion.
|
|
163
163
|
`pullEvents` returns structured rejection and collaborator action details. It
|
|
164
164
|
also returns merge summaries. Sync and cache signals mean the agent should
|
|
165
165
|
re-query after reconnect catch-up.
|
|
@@ -339,7 +339,7 @@ When the import fails (e.g. the server returns 400 because no components were ex
|
|
|
339
339
|
|
|
340
340
|
Commit these files. They bind the checkout to the Zoah project:
|
|
341
341
|
|
|
342
|
-
- `.zoah/config.json`:
|
|
342
|
+
- `.zoah/config.json`: organization and project binding, written on the first import.
|
|
343
343
|
- `.zoah/components.json`: component IDs, package exports, and slot anchors.
|
|
344
344
|
- `.zoah/manifest.json`: published package name, version, and registry URL.
|
|
345
345
|
|
package/dist/cli.mjs
CHANGED
|
@@ -467,29 +467,29 @@ var init_client = __esm({
|
|
|
467
467
|
);
|
|
468
468
|
}
|
|
469
469
|
/**
|
|
470
|
-
* Every
|
|
470
|
+
* Every organization the user belongs to (`GET /api/import/organizations`).
|
|
471
471
|
*
|
|
472
|
-
* The
|
|
472
|
+
* The organization is the outermost scope an agent works in and was the only one
|
|
473
473
|
* with no way to see it: responses carried an opaque `orgId` and the readable
|
|
474
474
|
* slug appeared only inside an app URL. Membership comes from the owner-group
|
|
475
475
|
* walk the sync gateway already performs on every socket identify, so this
|
|
476
|
-
* lists every
|
|
476
|
+
* lists every organization rather than the one the human last selected.
|
|
477
477
|
*/
|
|
478
|
-
|
|
478
|
+
listOrganizations() {
|
|
479
479
|
return this.send(
|
|
480
480
|
"GET",
|
|
481
481
|
"/api/import/organizations"
|
|
482
482
|
);
|
|
483
483
|
}
|
|
484
484
|
/**
|
|
485
|
-
* Create
|
|
485
|
+
* Create an organization (`POST /api/import/organizations`).
|
|
486
486
|
*
|
|
487
487
|
* The server creates it and keeps the org-capable credential to itself, so
|
|
488
488
|
* this call carries nothing but the name. The slug is derived server-side
|
|
489
489
|
* from that name: `slug` is globally unique, so a caller that invented one
|
|
490
490
|
* would be inventing a value it cannot check.
|
|
491
491
|
*/
|
|
492
|
-
|
|
492
|
+
createOrganization(name) {
|
|
493
493
|
return this.send(
|
|
494
494
|
"POST",
|
|
495
495
|
"/api/import/organizations",
|
|
@@ -53471,8 +53471,8 @@ var init_ai_action_style_keys = __esm({
|
|
|
53471
53471
|
|
|
53472
53472
|
// ../shared/dist/ai/opaque/opaqueActions.js
|
|
53473
53473
|
import { z as z51 } from "zod";
|
|
53474
|
-
function
|
|
53475
|
-
return
|
|
53474
|
+
function isOrganizationActionType(type) {
|
|
53475
|
+
return ORGANIZATION_ACTION_TYPES.includes(type);
|
|
53476
53476
|
}
|
|
53477
53477
|
function def(type, description, input, options) {
|
|
53478
53478
|
var _a2;
|
|
@@ -53485,7 +53485,7 @@ function def(type, description, input, options) {
|
|
|
53485
53485
|
function getOpaqueActionDef(type) {
|
|
53486
53486
|
return OPAQUE_ACTIONS.find((a5) => a5.type === type);
|
|
53487
53487
|
}
|
|
53488
|
-
var shaderLayersSchema, variableValue, STYLE_KEYS, STYLE_TRAPS, nodeUpdatesSchema,
|
|
53488
|
+
var shaderLayersSchema, variableValue, STYLE_KEYS, STYLE_TRAPS, nodeUpdatesSchema, ORGANIZATION_ACTION_TYPES, componentId, pageId, variantId, OPAQUE_ACTIONS;
|
|
53489
53489
|
var init_opaqueActions = __esm({
|
|
53490
53490
|
"../shared/dist/ai/opaque/opaqueActions.js"() {
|
|
53491
53491
|
"use strict";
|
|
@@ -53530,7 +53530,7 @@ var init_opaqueActions = __esm({
|
|
|
53530
53530
|
base: z51.record(z51.string(), z51.unknown()).optional().describe(`Visual properties. VALID KEYS \u2014 no others are accepted, and one invalid key rejects the WHOLE edit: ${STYLE_KEYS.join(", ")}. Common mistakes: ${STYLE_TRAPS}.`)
|
|
53531
53531
|
}).optional()
|
|
53532
53532
|
}).describe("Initial/updated values: `styles` for visual properties, `name` for the layer name, `innerText` for text content (text nodes only), `svgPath` to promote to vector, `shaderLayers` for shader effects, plus the semantic node fields (placeholder, inputValue, inputTextType, htmlTag, className, url, target, frameUrl).");
|
|
53533
|
-
|
|
53533
|
+
ORGANIZATION_ACTION_TYPES = [
|
|
53534
53534
|
"listProjects",
|
|
53535
53535
|
"createProject",
|
|
53536
53536
|
"updateProject",
|
|
@@ -53821,12 +53821,12 @@ var init_opaqueActions = __esm({
|
|
|
53821
53821
|
sourceBranch: z51.string().optional().describe("The branch whose OPEN pull request to comment on. Pass this OR prId."),
|
|
53822
53822
|
body: z51.string().describe("The comment, in plain text.")
|
|
53823
53823
|
})),
|
|
53824
|
-
def("listProjects", "List the projects in the user's current
|
|
53825
|
-
def("createProject", "Create a new project in the user's current
|
|
53824
|
+
def("listProjects", "List the projects in the user's current organization: id, name, slug, and last-update time. The [Organization] context block already carries this list \u2014 call this only when you need a FRESH read (e.g. right after you created a project).", z51.object({}), { readOnly: true }),
|
|
53825
|
+
def("createProject", "Create a new project in the user's current organization. Returns the new project's id and slug. Set `open: true` to also navigate the user's browser to it \u2014 do that only when the user asked to open or start working in it.", z51.object({
|
|
53826
53826
|
name: z51.string().describe("Display name for the new project."),
|
|
53827
53827
|
open: z51.boolean().optional().describe("True to navigate the user's browser to the new project. Only when the user asked to open it.")
|
|
53828
53828
|
})),
|
|
53829
|
-
def("updateProject", "Rename a project in the user's current
|
|
53829
|
+
def("updateProject", "Rename a project in the user's current organization. Never navigates.", z51.object({
|
|
53830
53830
|
projectRef: z51.string().describe("The target project's id, or its exact display name."),
|
|
53831
53831
|
name: z51.string().describe("The new display name.")
|
|
53832
53832
|
})),
|
|
@@ -53836,7 +53836,7 @@ var init_opaqueActions = __esm({
|
|
|
53836
53836
|
def("setTargetProject", "Point this turn's canvas verbs at a project WITHOUT navigating the user's browser. Later canvas edits run against that project in the background, on this agent's own work branch. Use this to act on a project the user did not ask to open.", z51.object({
|
|
53837
53837
|
projectRef: z51.string().describe("The target project's id, or its exact display name.")
|
|
53838
53838
|
})),
|
|
53839
|
-
def("createAgent", "Create a new agent for the user. With `projectId` the new agent is DEDICATED: it can act only inside that project, and the user finds it in that project's agent picker. Without `projectId` it is another
|
|
53839
|
+
def("createAgent", "Create a new agent for the user. With `projectId` the new agent is DEDICATED: it can act only inside that project, and the user finds it in that project's agent picker. Without `projectId` it is another organization agent. This does not start the new agent \u2014 the user sends its first message.", z51.object({
|
|
53840
53840
|
name: z51.string().optional().describe("Display name for the new agent (default: 'Agent N')."),
|
|
53841
53841
|
projectId: z51.string().optional().describe("Bind the new agent to this project id.")
|
|
53842
53842
|
})),
|
|
@@ -53943,7 +53943,7 @@ var init_brief_context = __esm({
|
|
|
53943
53943
|
});
|
|
53944
53944
|
|
|
53945
53945
|
// ../shared/dist/ai/conversation-personas.js
|
|
53946
|
-
var AGENT_PREAMBLE, MULTI_FAMILY_GUIDANCE, CONVERSATIONAL_PERSONA,
|
|
53946
|
+
var AGENT_PREAMBLE, MULTI_FAMILY_GUIDANCE, CONVERSATIONAL_PERSONA, ORGANIZATION_PERSONA, CANVAS_PERSONA, CANVAS_STYLES_SUPPLIED_NOTE;
|
|
53947
53947
|
var init_conversation_personas = __esm({
|
|
53948
53948
|
"../shared/dist/ai/conversation-personas.js"() {
|
|
53949
53949
|
"use strict";
|
|
@@ -53977,11 +53977,11 @@ uses the filesystem tools. Some tasks use more than one; that's fine.
|
|
|
53977
53977
|
" asks to build/generate a new brand or design system.",
|
|
53978
53978
|
"- You work inside THIS project only. If the user asks for work outside it \u2014",
|
|
53979
53979
|
" creating projects, or renaming or editing other projects \u2014 say that a",
|
|
53980
|
-
"
|
|
53980
|
+
" ORGANIZATION agent does that, in the agent picker's Organization section."
|
|
53981
53981
|
].join("\n");
|
|
53982
|
-
|
|
53983
|
-
"You are the user's
|
|
53984
|
-
"- The [
|
|
53982
|
+
ORGANIZATION_PERSONA = [
|
|
53983
|
+
"You are the user's ORGANIZATION agent. No project is open on screen.",
|
|
53984
|
+
"- The [Organization] block above lists the organization's projects (name, slug,",
|
|
53985
53985
|
" id). Resolve a project the user names from that list; ask only",
|
|
53986
53986
|
" when two projects genuinely collide. Call listProjects only for a FRESH",
|
|
53987
53987
|
" read (e.g. right after you created a project).",
|
|
@@ -54020,7 +54020,7 @@ uses the filesystem tools. Some tasks use more than one; that's fine.
|
|
|
54020
54020
|
" correct property. Fix and retry rather than reporting failure to the user.",
|
|
54021
54021
|
"- You work inside THIS project only. If the user asks for work outside it \u2014",
|
|
54022
54022
|
" creating projects, or renaming or editing other projects \u2014 say that a",
|
|
54023
|
-
"
|
|
54023
|
+
" ORGANIZATION agent does that, in the agent picker's Organization section."
|
|
54024
54024
|
].join("\n");
|
|
54025
54025
|
CANVAS_STYLES_SUPPLIED_NOTE = [
|
|
54026
54026
|
"- The selected element's CURRENT STYLES are listed above and are up to date.",
|
|
@@ -57607,7 +57607,7 @@ var init_page_generator = __esm({
|
|
|
57607
57607
|
});
|
|
57608
57608
|
|
|
57609
57609
|
// ../shared/dist/element-generation/components/adapters/deployment/app-runtime.js
|
|
57610
|
-
var NODE_HOOK_ATTR, INSTANCE_HOOK_ATTR, DATA_ATTR_PREFIX;
|
|
57610
|
+
var NODE_HOOK_ATTR, BUSY_CONTROL_KEY, INSTANCE_HOOK_ATTR, DATA_ATTR_PREFIX;
|
|
57611
57611
|
var init_app_runtime = __esm({
|
|
57612
57612
|
"../shared/dist/element-generation/components/adapters/deployment/app-runtime.js"() {
|
|
57613
57613
|
"use strict";
|
|
@@ -57615,6 +57615,7 @@ var init_app_runtime = __esm({
|
|
|
57615
57615
|
init_deployed_naming();
|
|
57616
57616
|
init_constants3();
|
|
57617
57617
|
NODE_HOOK_ATTR = dataAttr("node");
|
|
57618
|
+
BUSY_CONTROL_KEY = `__${DEPLOYED_NAMING.dataAttrPrefix}Control`;
|
|
57618
57619
|
INSTANCE_HOOK_ATTR = dataAttr("instance");
|
|
57619
57620
|
DATA_ATTR_PREFIX = DEPLOYED_NAMING.dataAttrPrefix;
|
|
57620
57621
|
}
|
|
@@ -88283,7 +88284,7 @@ var CLI_VERSION;
|
|
|
88283
88284
|
var init_version = __esm({
|
|
88284
88285
|
"src/config/version.ts"() {
|
|
88285
88286
|
"use strict";
|
|
88286
|
-
CLI_VERSION = true ? "0.1.
|
|
88287
|
+
CLI_VERSION = true ? "0.1.3" : "0.0.0-dev";
|
|
88287
88288
|
}
|
|
88288
88289
|
});
|
|
88289
88290
|
|
|
@@ -90638,13 +90639,13 @@ function serializeProject(project) {
|
|
|
90638
90639
|
slug: typeof project.slug === "string" ? project.slug : null
|
|
90639
90640
|
};
|
|
90640
90641
|
}
|
|
90641
|
-
async function
|
|
90642
|
+
async function loadOrganizationProjects() {
|
|
90642
90643
|
const matches = await Project2.query({}).reduce({});
|
|
90643
90644
|
return matches.filter((project) => !!project);
|
|
90644
90645
|
}
|
|
90645
90646
|
async function executeOrgActionAsync(action, context) {
|
|
90646
90647
|
var _a2, _b;
|
|
90647
|
-
const projects = await
|
|
90648
|
+
const projects = await loadOrganizationProjects();
|
|
90648
90649
|
if (action.type === "listProjects") {
|
|
90649
90650
|
return {
|
|
90650
90651
|
action,
|
|
@@ -90670,7 +90671,7 @@ async function executeOrgActionAsync(action, context) {
|
|
|
90670
90671
|
return {
|
|
90671
90672
|
action,
|
|
90672
90673
|
success: false,
|
|
90673
|
-
error: `No project "${ref2}" in this
|
|
90674
|
+
error: `No project "${ref2}" in this organization. Call listProjects to see what is there.`
|
|
90674
90675
|
};
|
|
90675
90676
|
}
|
|
90676
90677
|
const named = Object.assign(Object.assign({}, action), { projectRef: resolved.id });
|
|
@@ -90705,7 +90706,7 @@ function executeCreateProject(action, context, existing) {
|
|
|
90705
90706
|
return {
|
|
90706
90707
|
action,
|
|
90707
90708
|
success: false,
|
|
90708
|
-
error: `This
|
|
90709
|
+
error: `This organization already has a project called "${name}" (${taken.id}). Two projects with one name leave every later reference ambiguous.`
|
|
90709
90710
|
};
|
|
90710
90711
|
}
|
|
90711
90712
|
const actionIds = [];
|
|
@@ -96622,7 +96623,7 @@ var init_async_context = __esm({
|
|
|
96622
96623
|
});
|
|
96623
96624
|
|
|
96624
96625
|
// src/opaque/headless-client.ts
|
|
96625
|
-
function
|
|
96626
|
+
function isOrganizationClientUsable(client2) {
|
|
96626
96627
|
return client2.isWebSocketOpen() && client2.isOrgConfirmed();
|
|
96627
96628
|
}
|
|
96628
96629
|
function assertNodeWebSocket() {
|
|
@@ -96742,7 +96743,7 @@ async function resolveProjectRef(listing, reference) {
|
|
|
96742
96743
|
throw new ProjectRefError("Project reference is empty.");
|
|
96743
96744
|
}
|
|
96744
96745
|
const { projects } = listing;
|
|
96745
|
-
const
|
|
96746
|
+
const organization = listing.organization ?? (listing.orgId ? { id: listing.orgId, name: null, slug: null } : void 0);
|
|
96746
96747
|
if (PROJECT_ID_PATTERN.test(trimmed)) {
|
|
96747
96748
|
const mine = projects.find(
|
|
96748
96749
|
(p4) => p4.id.toLowerCase() === trimmed.toLowerCase()
|
|
@@ -96753,38 +96754,38 @@ async function resolveProjectRef(listing, reference) {
|
|
|
96753
96754
|
name: mine.name,
|
|
96754
96755
|
slug: mine.slug,
|
|
96755
96756
|
matchedBy: "id",
|
|
96756
|
-
|
|
96757
|
+
organization
|
|
96757
96758
|
};
|
|
96758
96759
|
}
|
|
96759
96760
|
throw new ProjectRefError(
|
|
96760
|
-
`${trimmed} is not among this
|
|
96761
|
+
`${trimmed} is not among this organization's projects. Call listProjects to see what is here, or pass \`organization\` to look somewhere else. Nothing was connected.`
|
|
96761
96762
|
);
|
|
96762
96763
|
}
|
|
96763
96764
|
const { record, matchedBy } = matchRef({
|
|
96764
96765
|
reference: trimmed,
|
|
96765
96766
|
records: projects,
|
|
96766
96767
|
noun: "project",
|
|
96767
|
-
emptyMessage: "This
|
|
96768
|
-
candidatesPrefix: "This
|
|
96768
|
+
emptyMessage: "This organization has no projects. Call createProject, then connect to it.",
|
|
96769
|
+
candidatesPrefix: "This organization's projects"
|
|
96769
96770
|
});
|
|
96770
96771
|
return {
|
|
96771
96772
|
projectId: record.id,
|
|
96772
96773
|
name: record.name,
|
|
96773
96774
|
slug: record.slug,
|
|
96774
96775
|
matchedBy,
|
|
96775
|
-
|
|
96776
|
+
organization
|
|
96776
96777
|
};
|
|
96777
96778
|
}
|
|
96778
|
-
async function
|
|
96779
|
+
async function resolveOrganizationRef(api, reference) {
|
|
96779
96780
|
const trimmed = reference.trim();
|
|
96780
96781
|
if (!trimmed) {
|
|
96781
|
-
throw new ProjectRefError("
|
|
96782
|
+
throw new ProjectRefError("Organization reference is empty.");
|
|
96782
96783
|
}
|
|
96783
|
-
const { organizations } = await api.
|
|
96784
|
+
const { organizations } = await api.listOrganizations();
|
|
96784
96785
|
if (ORG_ID_PATTERN.test(trimmed)) {
|
|
96785
96786
|
const known = organizations.find((o3) => o3.id === trimmed);
|
|
96786
96787
|
return {
|
|
96787
|
-
|
|
96788
|
+
organizationId: trimmed,
|
|
96788
96789
|
...known ? { name: known.name, slug: known.slug } : {},
|
|
96789
96790
|
role: known?.role ?? "read",
|
|
96790
96791
|
matchedBy: "id"
|
|
@@ -96793,12 +96794,12 @@ async function resolveWorkspaceRef(api, reference) {
|
|
|
96793
96794
|
const { record, matchedBy } = matchRef({
|
|
96794
96795
|
reference: trimmed,
|
|
96795
96796
|
records: organizations,
|
|
96796
|
-
noun: "
|
|
96797
|
-
emptyMessage: "This account belongs to no
|
|
96798
|
-
candidatesPrefix: "Your
|
|
96797
|
+
noun: "organization",
|
|
96798
|
+
emptyMessage: "This account belongs to no organization yet.",
|
|
96799
|
+
candidatesPrefix: "Your organizations"
|
|
96799
96800
|
});
|
|
96800
96801
|
return {
|
|
96801
|
-
|
|
96802
|
+
organizationId: record.id,
|
|
96802
96803
|
name: record.name,
|
|
96803
96804
|
slug: record.slug,
|
|
96804
96805
|
role: record.role,
|
|
@@ -97363,12 +97364,12 @@ var init_ai_action_api = __esm({
|
|
|
97363
97364
|
},
|
|
97364
97365
|
{
|
|
97365
97366
|
type: "listProjects",
|
|
97366
|
-
description: "List the projects in the user's current
|
|
97367
|
+
description: "List the projects in the user's current organization: id, name, slug, and last-update time. The [Organization] context block already carries this list \u2014 call this only when you need a FRESH read (e.g. right after you created a project).",
|
|
97367
97368
|
parameters: {}
|
|
97368
97369
|
},
|
|
97369
97370
|
{
|
|
97370
97371
|
type: "createProject",
|
|
97371
|
-
description: "Create a new project in the user's current
|
|
97372
|
+
description: "Create a new project in the user's current organization. Returns the new project's id and slug. Set `open: true` to also navigate the user's browser to it \u2014 do that only when the user asked to open or start working in it.",
|
|
97372
97373
|
parameters: {
|
|
97373
97374
|
name: "string - Display name for the new project.",
|
|
97374
97375
|
open: "boolean (optional) - True to navigate the user's browser to the new project. Only when the user asked to open it."
|
|
@@ -97376,7 +97377,7 @@ var init_ai_action_api = __esm({
|
|
|
97376
97377
|
},
|
|
97377
97378
|
{
|
|
97378
97379
|
type: "updateProject",
|
|
97379
|
-
description: "Rename a project in the user's current
|
|
97380
|
+
description: "Rename a project in the user's current organization. Never navigates.",
|
|
97380
97381
|
parameters: {
|
|
97381
97382
|
projectRef: "string - The target project's id, or its exact display name.",
|
|
97382
97383
|
name: "string - The new display name."
|
|
@@ -97398,7 +97399,7 @@ var init_ai_action_api = __esm({
|
|
|
97398
97399
|
},
|
|
97399
97400
|
{
|
|
97400
97401
|
type: "createAgent",
|
|
97401
|
-
description: "Create a new agent for the user. With `projectId` the new agent is DEDICATED: it can act only inside that project, and the user finds it in that project's agent picker. Without `projectId` it is another
|
|
97402
|
+
description: "Create a new agent for the user. With `projectId` the new agent is DEDICATED: it can act only inside that project, and the user finds it in that project's agent picker. Without `projectId` it is another organization agent. This does not start the new agent \u2014 the user sends its first message.",
|
|
97402
97403
|
parameters: {
|
|
97403
97404
|
name: "string (optional) - Display name for the new agent (default: 'Agent N').",
|
|
97404
97405
|
projectId: "string (optional) - Bind the new agent to this project id."
|
|
@@ -98603,13 +98604,13 @@ async function runMcpServer(options) {
|
|
|
98603
98604
|
options.baseUrlOverride ?? creds.baseUrl ?? BUILT_IN_BASE_URL
|
|
98604
98605
|
);
|
|
98605
98606
|
if (credentialInstance === session.baseUrl) return null;
|
|
98606
|
-
return `The credential now points at ${credentialInstance}, but this session is open against ${session.baseUrl}. Refusing to send this call to a different instance than the one you are working on: the token would be presented to the wrong server, and on a development machine the two are commonly local dev and production. Nothing was sent. Either restore the credential for ${session.baseUrl} (\`zoah login\` in that
|
|
98607
|
+
return `The credential now points at ${credentialInstance}, but this session is open against ${session.baseUrl}. Refusing to send this call to a different instance than the one you are working on: the token would be presented to the wrong server, and on a development machine the two are commonly local dev and production. Nothing was sent. Either restore the credential for ${session.baseUrl} (\`zoah login\` in that directory), or call \`disconnect\` and then \`connect\` to move this session to ${credentialInstance} deliberately.`;
|
|
98607
98608
|
}
|
|
98608
98609
|
function loginScope() {
|
|
98609
98610
|
return existsSync10(zoahPaths.localCredentials(process.cwd())) || existsSync10(legacyOpacityPaths.localCredentials(process.cwd())) ? "local" : "global";
|
|
98610
98611
|
}
|
|
98611
98612
|
let pendingSignIn = null;
|
|
98612
|
-
let
|
|
98613
|
+
let selectedOrganization = null;
|
|
98613
98614
|
let sessionEndedBy = null;
|
|
98614
98615
|
function noSessionMessage() {
|
|
98615
98616
|
return (sessionEndedBy ? `No session: ${sessionEndedBy}. ` : "No session. ") + "Call `connect` with `project` (a slug, a name or an id) to open one; add `branch` to pick a branch, or omit it to work on a fresh agent branch.";
|
|
@@ -98666,18 +98667,18 @@ async function runMcpServer(options) {
|
|
|
98666
98667
|
return result;
|
|
98667
98668
|
}
|
|
98668
98669
|
}
|
|
98669
|
-
let
|
|
98670
|
+
let ambientOrganization = null;
|
|
98670
98671
|
function describeWorkingIn(fallbackId) {
|
|
98671
|
-
if (
|
|
98672
|
+
if (selectedOrganization) {
|
|
98672
98673
|
return {
|
|
98673
|
-
id:
|
|
98674
|
-
name:
|
|
98675
|
-
slug:
|
|
98674
|
+
id: selectedOrganization.organizationId,
|
|
98675
|
+
name: selectedOrganization.name ?? null,
|
|
98676
|
+
slug: selectedOrganization.slug ?? null,
|
|
98676
98677
|
pinnedBy: "connect"
|
|
98677
98678
|
};
|
|
98678
98679
|
}
|
|
98679
|
-
if (
|
|
98680
|
-
return { ...
|
|
98680
|
+
if (ambientOrganization) {
|
|
98681
|
+
return { ...ambientOrganization, pinnedBy: "the human's selection" };
|
|
98681
98682
|
}
|
|
98682
98683
|
return fallbackId ? {
|
|
98683
98684
|
id: fallbackId,
|
|
@@ -98686,57 +98687,60 @@ async function runMcpServer(options) {
|
|
|
98686
98687
|
pinnedBy: "the human's selection"
|
|
98687
98688
|
} : null;
|
|
98688
98689
|
}
|
|
98689
|
-
function
|
|
98690
|
+
function organizationLabel(fallbackId) {
|
|
98690
98691
|
const working = describeWorkingIn(fallbackId);
|
|
98691
98692
|
return working?.slug ?? working?.id ?? fallbackId;
|
|
98692
98693
|
}
|
|
98693
|
-
function
|
|
98694
|
+
function describeSelectedOrganization() {
|
|
98694
98695
|
const working = describeWorkingIn();
|
|
98695
|
-
return working ? {
|
|
98696
|
+
return working ? { organization: working } : {};
|
|
98696
98697
|
}
|
|
98697
|
-
async function
|
|
98698
|
+
async function pinOrganization(reference) {
|
|
98698
98699
|
if (!reference) return {};
|
|
98699
|
-
const resolved = await
|
|
98700
|
-
|
|
98701
|
-
|
|
98700
|
+
const resolved = await resolveOrganizationRef(
|
|
98701
|
+
await currentApi(),
|
|
98702
|
+
reference
|
|
98703
|
+
);
|
|
98704
|
+
const droppedSession = resolved.organizationId !== selectedOrganization?.organizationId ? await endSessionForOrganizationChange(resolved) : void 0;
|
|
98705
|
+
selectedOrganization = resolved;
|
|
98702
98706
|
applyRole(resolved.role);
|
|
98703
98707
|
return droppedSession ? { droppedSession } : {};
|
|
98704
98708
|
}
|
|
98705
|
-
async function
|
|
98709
|
+
async function endSessionForOrganizationChange(to) {
|
|
98706
98710
|
const dropped = session ? {
|
|
98707
98711
|
project: session.projectId,
|
|
98708
98712
|
branch: session.branchSlug,
|
|
98709
|
-
|
|
98710
|
-
|
|
98713
|
+
fromOrganization: organizationLabel(session.orgId),
|
|
98714
|
+
toOrganization: to.slug ?? to.organizationId
|
|
98711
98715
|
} : void 0;
|
|
98712
98716
|
if (session) await teardownSession();
|
|
98713
|
-
await
|
|
98717
|
+
await teardownOrganizationClient();
|
|
98714
98718
|
if (!dropped) return void 0;
|
|
98715
|
-
sessionEndedBy = `switching to
|
|
98719
|
+
sessionEndedBy = `switching to organization ${dropped.toOrganization} ended the session on ${dropped.project} (${dropped.branch}), because that session's token was minted for a project in ${dropped.fromOrganization}`;
|
|
98716
98720
|
return dropped;
|
|
98717
98721
|
}
|
|
98718
98722
|
const syncUrlOverride = options.syncUrlOverride ?? process.env.ZOAH_SYNC_WS_URL ?? process.env.OPACITY_SYNC_WS_URL;
|
|
98719
98723
|
let session = null;
|
|
98720
|
-
let
|
|
98724
|
+
let organizationClient = null;
|
|
98721
98725
|
function applyClientRole(client2, role) {
|
|
98722
98726
|
let applied = false;
|
|
98723
98727
|
if (session?.client === client2) {
|
|
98724
98728
|
session.role = role;
|
|
98725
98729
|
applied = true;
|
|
98726
98730
|
}
|
|
98727
|
-
if (
|
|
98728
|
-
|
|
98731
|
+
if (organizationClient?.client === client2) {
|
|
98732
|
+
organizationClient.role = role;
|
|
98729
98733
|
applied = true;
|
|
98730
98734
|
}
|
|
98731
98735
|
if (applied) applyRole(role);
|
|
98732
98736
|
}
|
|
98733
|
-
async function
|
|
98734
|
-
if (!
|
|
98735
|
-
const { client: client2 } =
|
|
98736
|
-
|
|
98737
|
+
async function teardownOrganizationClient() {
|
|
98738
|
+
if (!organizationClient) return;
|
|
98739
|
+
const { client: client2 } = organizationClient;
|
|
98740
|
+
organizationClient = null;
|
|
98737
98741
|
await client2.disconnectAndWait();
|
|
98738
98742
|
}
|
|
98739
|
-
async function
|
|
98743
|
+
async function ensureOrganizationClient() {
|
|
98740
98744
|
if (session) {
|
|
98741
98745
|
return {
|
|
98742
98746
|
client: session.client,
|
|
@@ -98744,15 +98748,15 @@ async function runMcpServer(options) {
|
|
|
98744
98748
|
role: session.role
|
|
98745
98749
|
};
|
|
98746
98750
|
}
|
|
98747
|
-
if (
|
|
98748
|
-
let orgId =
|
|
98751
|
+
if (organizationClient) return organizationClient;
|
|
98752
|
+
let orgId = selectedOrganization?.organizationId;
|
|
98749
98753
|
if (!orgId) {
|
|
98750
|
-
const { organizations, activeOrganizationId } = await (await currentApi()).
|
|
98754
|
+
const { organizations, activeOrganizationId } = await (await currentApi()).listOrganizations();
|
|
98751
98755
|
orgId = activeOrganizationId ?? (organizations.length === 1 ? organizations[0]?.id : void 0);
|
|
98752
98756
|
}
|
|
98753
98757
|
if (!orgId) {
|
|
98754
98758
|
throw new Error(
|
|
98755
|
-
"No
|
|
98759
|
+
"No organization selected, and you belong to more than one. Call listOrganizations to see them, then connect with `organization` to choose one."
|
|
98756
98760
|
);
|
|
98757
98761
|
}
|
|
98758
98762
|
let minted = await (await currentApi()).mintAgentSyncToken({ orgId });
|
|
@@ -98770,14 +98774,14 @@ async function runMcpServer(options) {
|
|
|
98770
98774
|
return minted.token;
|
|
98771
98775
|
}
|
|
98772
98776
|
});
|
|
98773
|
-
|
|
98777
|
+
organizationClient = {
|
|
98774
98778
|
client: client2,
|
|
98775
98779
|
orgId: minted.orgId,
|
|
98776
98780
|
role: minted.role,
|
|
98777
98781
|
getSyncTokenExpiresAt: () => minted.expiresAt
|
|
98778
98782
|
};
|
|
98779
98783
|
applyRole(minted.role);
|
|
98780
|
-
return
|
|
98784
|
+
return organizationClient;
|
|
98781
98785
|
}
|
|
98782
98786
|
async function teardownSession() {
|
|
98783
98787
|
if (!session) return;
|
|
@@ -98785,7 +98789,7 @@ async function runMcpServer(options) {
|
|
|
98785
98789
|
const { client: client2 } = session;
|
|
98786
98790
|
session = null;
|
|
98787
98791
|
await client2.disconnectAndWait();
|
|
98788
|
-
applyRole(
|
|
98792
|
+
applyRole(selectedOrganization?.role ?? null);
|
|
98789
98793
|
}
|
|
98790
98794
|
function pushEvent(event) {
|
|
98791
98795
|
if (!session) return;
|
|
@@ -98801,15 +98805,15 @@ async function runMcpServer(options) {
|
|
|
98801
98805
|
let minted = await (await currentApi()).mintAgentSyncToken({
|
|
98802
98806
|
projectId,
|
|
98803
98807
|
branchSlug,
|
|
98804
|
-
// ORG-scoped: the token names the
|
|
98805
|
-
// The gateway confines this token to projects owned by the
|
|
98808
|
+
// ORG-scoped: the token names the organization, not this project. The
|
|
98809
|
+
// The gateway confines this token to projects owned by the organization.
|
|
98806
98810
|
// The session selects one project through its socket context.
|
|
98807
98811
|
orgScoped: true
|
|
98808
98812
|
});
|
|
98809
98813
|
const wsUrl = syncUrlOverride ?? minted.syncUrl ?? deriveSyncWsUrl(await currentBaseUrl());
|
|
98810
|
-
const adopted =
|
|
98814
|
+
const adopted = organizationClient && organizationClient.orgId === minted.orgId ? organizationClient : null;
|
|
98811
98815
|
if (adopted) {
|
|
98812
|
-
|
|
98816
|
+
organizationClient = null;
|
|
98813
98817
|
try {
|
|
98814
98818
|
await enterProjectScope(adopted.client, {
|
|
98815
98819
|
orgId: minted.orgId,
|
|
@@ -98965,7 +98969,7 @@ async function runMcpServer(options) {
|
|
|
98965
98969
|
const def3 = OPAQUE_ACTIONS.find((candidate) => candidate.type === type);
|
|
98966
98970
|
if ((def3?.mutates ?? true) && s5.role === "read") {
|
|
98967
98971
|
throw new Error(
|
|
98968
|
-
`${type}: this
|
|
98972
|
+
`${type}: this organization role is read-only. NOTHING was written.`
|
|
98969
98973
|
);
|
|
98970
98974
|
}
|
|
98971
98975
|
const wasDown = !socketUsable(s5);
|
|
@@ -99003,17 +99007,17 @@ async function runMcpServer(options) {
|
|
|
99003
99007
|
await client2.waitForActionAck(actionId, VERB_ACK_TIMEOUT_MS);
|
|
99004
99008
|
}
|
|
99005
99009
|
}
|
|
99006
|
-
async function
|
|
99007
|
-
const { client: client2, orgId, role } = await
|
|
99010
|
+
async function runOrganizationVerb(type, input) {
|
|
99011
|
+
const { client: client2, orgId, role } = await ensureOrganizationClient();
|
|
99008
99012
|
const def3 = OPAQUE_ACTIONS.find((candidate) => candidate.type === type);
|
|
99009
99013
|
if ((def3?.mutates ?? true) && role === "read") {
|
|
99010
99014
|
throw new Error(
|
|
99011
|
-
`${type}: this
|
|
99015
|
+
`${type}: this organization role is read-only. NOTHING was written.`
|
|
99012
99016
|
);
|
|
99013
99017
|
}
|
|
99014
|
-
if ((def3?.mutates ?? true) && await awaitLiveClient(client2,
|
|
99018
|
+
if ((def3?.mutates ?? true) && await awaitLiveClient(client2, isOrganizationClientUsable) === "down") {
|
|
99015
99019
|
throw new Error(
|
|
99016
|
-
`${type}: the
|
|
99020
|
+
`${type}: the organization sync connection is down. NOTHING was written. Nothing is queued or applied later. Call this verb again after the connection returns.`
|
|
99017
99021
|
);
|
|
99018
99022
|
}
|
|
99019
99023
|
return client2.runWithSchemas(async () => {
|
|
@@ -99024,7 +99028,7 @@ async function runMcpServer(options) {
|
|
|
99024
99028
|
return {
|
|
99025
99029
|
action: { type },
|
|
99026
99030
|
success: false,
|
|
99027
|
-
error: !org ? `
|
|
99031
|
+
error: !org ? `Organization ${orgId} could not be loaded, so ${type} has nothing to act on.` : `This session has no signed-in user, so ${type} cannot run.`
|
|
99028
99032
|
};
|
|
99029
99033
|
}
|
|
99030
99034
|
const action = { type, ...input };
|
|
@@ -99033,20 +99037,20 @@ async function runMcpServer(options) {
|
|
|
99033
99037
|
return executed;
|
|
99034
99038
|
});
|
|
99035
99039
|
}
|
|
99036
|
-
async function
|
|
99037
|
-
const executed = await
|
|
99040
|
+
async function organizationListing() {
|
|
99041
|
+
const executed = await runOrganizationVerb("listProjects", {});
|
|
99038
99042
|
if (!executed.success) {
|
|
99039
99043
|
throw new Error(executed.error ?? "listProjects failed");
|
|
99040
99044
|
}
|
|
99041
99045
|
const { projects } = executed.data;
|
|
99042
99046
|
return {
|
|
99043
99047
|
projects,
|
|
99044
|
-
|
|
99045
|
-
id:
|
|
99046
|
-
name:
|
|
99047
|
-
slug:
|
|
99048
|
+
organization: selectedOrganization ? {
|
|
99049
|
+
id: selectedOrganization.organizationId,
|
|
99050
|
+
name: selectedOrganization.name ?? null,
|
|
99051
|
+
slug: selectedOrganization.slug ?? null
|
|
99048
99052
|
} : null,
|
|
99049
|
-
orgId:
|
|
99053
|
+
orgId: selectedOrganization?.organizationId ?? null
|
|
99050
99054
|
};
|
|
99051
99055
|
}
|
|
99052
99056
|
function reviewUrlFor(s5, prId) {
|
|
@@ -99058,7 +99062,7 @@ async function runMcpServer(options) {
|
|
|
99058
99062
|
if (!dropped) return {};
|
|
99059
99063
|
return {
|
|
99060
99064
|
droppedSession: { project: dropped.project, branch: dropped.branch },
|
|
99061
|
-
droppedSessionReason: `Switching to
|
|
99065
|
+
droppedSessionReason: `Switching to organization ${dropped.toOrganization} ended the session on ${dropped.project} (${dropped.branch}): that session's token was minted for a project in ${dropped.fromOrganization}, so keeping it open would leave the next write landing in an organization you have stopped talking about. Every verb that returned success was confirmed by the server before it returned. Open a session here with \`project\`.`
|
|
99062
99066
|
};
|
|
99063
99067
|
}
|
|
99064
99068
|
const server = new McpServer({ name: "zoah", version: CLI_VERSION });
|
|
@@ -99087,8 +99091,8 @@ async function runMcpServer(options) {
|
|
|
99087
99091
|
"Name what to open: pass `project` (a slug, a name or an id). Call listProjects to see what is available, or run `zoah mcp init` in a repo linked to a project to bake in a default."
|
|
99088
99092
|
);
|
|
99089
99093
|
}
|
|
99090
|
-
resolved = await resolveProjectRef(await
|
|
99091
|
-
if (resolved.
|
|
99094
|
+
resolved = await resolveProjectRef(await organizationListing(), named);
|
|
99095
|
+
if (resolved.organization) ambientOrganization = resolved.organization;
|
|
99092
99096
|
const projectId = resolved.projectId;
|
|
99093
99097
|
await teardownSession();
|
|
99094
99098
|
if (options.readOnly || branch) {
|
|
@@ -99134,10 +99138,10 @@ async function runMcpServer(options) {
|
|
|
99134
99138
|
sessionEndedBy = null;
|
|
99135
99139
|
return jsonResult({
|
|
99136
99140
|
connected: true,
|
|
99137
|
-
// Which
|
|
99138
|
-
// same project name in two
|
|
99141
|
+
// Which organization this project is in, named rather than implied. The
|
|
99142
|
+
// same project name in two organizations is two different projects, so a
|
|
99139
99143
|
// connected result that omits it is ambiguous by construction.
|
|
99140
|
-
|
|
99144
|
+
organization: describeWorkingIn(session.orgId),
|
|
99141
99145
|
role: session.role,
|
|
99142
99146
|
projectId,
|
|
99143
99147
|
// Name what the reference resolved to. A partial match that lands on
|
|
@@ -99168,13 +99172,13 @@ async function runMcpServer(options) {
|
|
|
99168
99172
|
server.registerTool(
|
|
99169
99173
|
"connect",
|
|
99170
99174
|
{
|
|
99171
|
-
description: "Choose what you are working on:
|
|
99175
|
+
description: "Choose what you are working on: organization, project, and branch. Call it to start, and again whenever you want to switch any of the three. It signs you in first if it has to, so a session usually needs nothing else. Safe to call at any time: it reports which state it is in rather than assuming one. When it returns an `authUrl`, give that URL to the human, wait for them to approve it, then call `connect` again - do not open it yourself and do not ask them for a token.",
|
|
99172
99176
|
inputSchema: {
|
|
99173
99177
|
baseUrl: z57.string().optional().describe(
|
|
99174
99178
|
"The Zoah instance to use, e.g. http://localhost:5071. Wins over the launch flag, the saved credential and the built-in default, and is saved so later calls agree. Needed on a clean workspace: with no credential file yet, the built-in default is a guess, and on a multi-instance dev machine it is usually the wrong port."
|
|
99175
99179
|
),
|
|
99176
|
-
|
|
99177
|
-
"
|
|
99180
|
+
organization: z57.string().optional().describe(
|
|
99181
|
+
"Organization to work in: its slug, its name, or its id. Stays selected for the rest of this session, so later calls and listProjects all scope to it. Omit to follow the human's own selection. Call listOrganizations to see them."
|
|
99178
99182
|
),
|
|
99179
99183
|
project: z57.string().optional().describe(
|
|
99180
99184
|
"Project to connect to: its slug, its name, or its id. Omit it to sign in only." + (options.defaultProject ? ` Defaults to this repo's linked project (${options.defaultProject}).` : " Call listProjects to see what is available.")
|
|
@@ -99184,7 +99188,7 @@ async function runMcpServer(options) {
|
|
|
99184
99188
|
)
|
|
99185
99189
|
}
|
|
99186
99190
|
},
|
|
99187
|
-
async ({ baseUrl,
|
|
99191
|
+
async ({ baseUrl, organization, project, branch }) => {
|
|
99188
99192
|
try {
|
|
99189
99193
|
const creds = await currentCreds().catch(() => null);
|
|
99190
99194
|
const target = resolveBaseUrl({
|
|
@@ -99196,7 +99200,7 @@ async function runMcpServer(options) {
|
|
|
99196
99200
|
const health = creds ? credentialHealth(creds.token) : null;
|
|
99197
99201
|
const usable = creds !== null && health !== null && !health.expired && !health.expiringSoon && stripSlash(creds.baseUrl) === target.baseUrl;
|
|
99198
99202
|
if (usable) {
|
|
99199
|
-
const pin = await
|
|
99203
|
+
const pin = await pinOrganization(organization);
|
|
99200
99204
|
if (!project && !options.defaultProject) {
|
|
99201
99205
|
return jsonResult({
|
|
99202
99206
|
signedIn: true,
|
|
@@ -99204,9 +99208,9 @@ async function runMcpServer(options) {
|
|
|
99204
99208
|
signingInTo: target.baseUrl,
|
|
99205
99209
|
baseUrlSource: target.source,
|
|
99206
99210
|
credentialExpiresInSeconds: health.expiresInSeconds,
|
|
99207
|
-
...
|
|
99211
|
+
...describeSelectedOrganization(),
|
|
99208
99212
|
...describeDrop(pin),
|
|
99209
|
-
note:
|
|
99213
|
+
note: selectedOrganization !== null ? `Already signed in, and working in ${selectedOrganization.slug ?? selectedOrganization.organizationId}. Pass \`project\` to connect to one, or call listProjects to see what is there.` : "Already signed in. Pass `project` to connect to one, or call listProjects to see what is available."
|
|
99210
99214
|
});
|
|
99211
99215
|
}
|
|
99212
99216
|
const connected = await connectAndDescribe({ project, branch });
|
|
@@ -99228,7 +99232,7 @@ async function runMcpServer(options) {
|
|
|
99228
99232
|
}),
|
|
99229
99233
|
startedAtMs: Date.now(),
|
|
99230
99234
|
baseUrl: target.baseUrl,
|
|
99231
|
-
|
|
99235
|
+
organization,
|
|
99232
99236
|
project,
|
|
99233
99237
|
branch
|
|
99234
99238
|
};
|
|
@@ -99258,14 +99262,16 @@ async function runMcpServer(options) {
|
|
|
99258
99262
|
pendingSignIn = null;
|
|
99259
99263
|
const wanted = project ?? pending.project;
|
|
99260
99264
|
const wantedBranch = branch ?? pending.branch;
|
|
99261
|
-
const pin = await
|
|
99265
|
+
const pin = await pinOrganization(
|
|
99266
|
+
organization ?? pending.organization
|
|
99267
|
+
);
|
|
99262
99268
|
if (!wanted && !options.defaultProject) {
|
|
99263
99269
|
return jsonResult({
|
|
99264
99270
|
signedIn: true,
|
|
99265
99271
|
userEmail,
|
|
99266
99272
|
signingInTo: target.baseUrl,
|
|
99267
99273
|
baseUrlSource: target.source,
|
|
99268
|
-
...
|
|
99274
|
+
...describeSelectedOrganization(),
|
|
99269
99275
|
...describeDrop(pin),
|
|
99270
99276
|
note: "Signed in. Pass `project` to connect to one, or call listProjects to see what is available."
|
|
99271
99277
|
});
|
|
@@ -99309,57 +99315,57 @@ async function runMcpServer(options) {
|
|
|
99309
99315
|
}
|
|
99310
99316
|
);
|
|
99311
99317
|
server.registerTool(
|
|
99312
|
-
"
|
|
99318
|
+
"listOrganizations",
|
|
99313
99319
|
{
|
|
99314
|
-
description: "List the Zoah
|
|
99320
|
+
description: "List the Zoah organizations you belong to (id, name, slug), and say which one this session is working in. An organization holds projects; anything in another organization is invisible until you switch, and reads as 'not found' rather than 'elsewhere'. Switch with connect({ organization }).",
|
|
99315
99321
|
inputSchema: {}
|
|
99316
99322
|
},
|
|
99317
99323
|
async () => {
|
|
99318
99324
|
try {
|
|
99319
|
-
const { organizations, activeOrganizationId } = await (await currentApi()).
|
|
99325
|
+
const { organizations, activeOrganizationId } = await (await currentApi()).listOrganizations();
|
|
99320
99326
|
return jsonResult({
|
|
99321
|
-
|
|
99322
|
-
...
|
|
99327
|
+
organizations: organizations.map((organization) => ({
|
|
99328
|
+
...organization,
|
|
99323
99329
|
// Two different meanings of "current", kept apart on purpose. The
|
|
99324
99330
|
// human's stored selection decides where a call that names no
|
|
99325
|
-
//
|
|
99331
|
+
// organization lands; this session's pin decides where THIS session
|
|
99326
99332
|
// works. Collapsing them into one flag would hide which of the two
|
|
99327
99333
|
// a caller is looking at.
|
|
99328
|
-
active:
|
|
99329
|
-
workingHere:
|
|
99334
|
+
active: organization.id === activeOrganizationId,
|
|
99335
|
+
workingHere: selectedOrganization ? organization.id === selectedOrganization.organizationId : organization.id === activeOrganizationId
|
|
99330
99336
|
})),
|
|
99331
99337
|
workingIn: describeWorkingIn(activeOrganizationId ?? void 0),
|
|
99332
99338
|
...organizations.length === 0 ? {
|
|
99333
|
-
note: "This account belongs to no
|
|
99339
|
+
note: "This account belongs to no organization yet. Call createOrganization to make one. Projects live inside an organization, so nothing else can be created first."
|
|
99334
99340
|
} : {}
|
|
99335
99341
|
});
|
|
99336
99342
|
} catch (error) {
|
|
99337
99343
|
return errorResult2(
|
|
99338
|
-
`
|
|
99344
|
+
`listOrganizations failed: ${describeError2(error)}`
|
|
99339
99345
|
);
|
|
99340
99346
|
}
|
|
99341
99347
|
}
|
|
99342
99348
|
);
|
|
99343
99349
|
if (!options.readOnly) {
|
|
99344
99350
|
server.registerTool(
|
|
99345
|
-
"
|
|
99351
|
+
"createOrganization",
|
|
99346
99352
|
{
|
|
99347
|
-
description: "Create a new Zoah
|
|
99353
|
+
description: "Create a new Zoah organization and work in it. An organization holds projects, so this is the first step on an account that has none. createProject needs an organization to hold the project. This session switches to it, which drops any open session. Ask the human before creating one if they may already have an organization you have not looked at: call listOrganizations first.",
|
|
99348
99354
|
inputSchema: {
|
|
99349
99355
|
name: z57.string().describe(
|
|
99350
|
-
'Name for the
|
|
99356
|
+
'Name for the organization, e.g. "Acme Design". The slug is derived from it server-side.'
|
|
99351
99357
|
)
|
|
99352
99358
|
}
|
|
99353
99359
|
},
|
|
99354
99360
|
async ({ name }) => {
|
|
99355
99361
|
try {
|
|
99356
|
-
const created = await (await currentApi()).
|
|
99357
|
-
const dropped = await
|
|
99358
|
-
|
|
99362
|
+
const created = await (await currentApi()).createOrganization(name);
|
|
99363
|
+
const dropped = await endSessionForOrganizationChange({
|
|
99364
|
+
organizationId: created.id,
|
|
99359
99365
|
slug: created.slug
|
|
99360
99366
|
});
|
|
99361
|
-
|
|
99362
|
-
|
|
99367
|
+
selectedOrganization = {
|
|
99368
|
+
organizationId: created.id,
|
|
99363
99369
|
name: created.name,
|
|
99364
99370
|
slug: created.slug,
|
|
99365
99371
|
role: "admin",
|
|
@@ -99367,23 +99373,23 @@ async function runMcpServer(options) {
|
|
|
99367
99373
|
};
|
|
99368
99374
|
applyRole("admin");
|
|
99369
99375
|
return jsonResult({
|
|
99370
|
-
|
|
99376
|
+
organization: {
|
|
99371
99377
|
id: created.id,
|
|
99372
99378
|
name: created.name,
|
|
99373
99379
|
slug: created.slug
|
|
99374
99380
|
},
|
|
99375
99381
|
workingHere: true,
|
|
99376
99382
|
...describeDrop({ droppedSession: dropped }),
|
|
99377
|
-
...created.
|
|
99378
|
-
|
|
99379
|
-
note: "This account had no
|
|
99383
|
+
...created.becameActiveOrganization ? {
|
|
99384
|
+
becameActiveOrganization: true,
|
|
99385
|
+
note: "This account had no organization, so this one is now its default - the app will open onto it. Create a project with createProject, then call connect to start work."
|
|
99380
99386
|
} : {
|
|
99381
|
-
note: "Created and selected for this session. The human's own default
|
|
99387
|
+
note: "Created and selected for this session. The human's own default organization is unchanged. Create a project with createProject, then call connect to start work."
|
|
99382
99388
|
}
|
|
99383
99389
|
});
|
|
99384
99390
|
} catch (error) {
|
|
99385
99391
|
return errorResult2(
|
|
99386
|
-
`
|
|
99392
|
+
`createOrganization failed: ${describeError2(error)}`
|
|
99387
99393
|
);
|
|
99388
99394
|
}
|
|
99389
99395
|
}
|
|
@@ -99392,14 +99398,14 @@ async function runMcpServer(options) {
|
|
|
99392
99398
|
server.registerTool(
|
|
99393
99399
|
"listProjects",
|
|
99394
99400
|
{
|
|
99395
|
-
description: "List the projects in the Zoah
|
|
99401
|
+
description: "List the projects in the Zoah organization this session is working in (id, name, slug), and name that organization. Call this before connect to discover what you can connect to. Connect accepts the slug, the name, or the id. Call listOrganizations to work elsewhere.",
|
|
99396
99402
|
inputSchema: {}
|
|
99397
99403
|
},
|
|
99398
99404
|
async () => {
|
|
99399
99405
|
try {
|
|
99400
|
-
const result = await
|
|
99406
|
+
const result = await organizationListing();
|
|
99401
99407
|
return jsonResult({
|
|
99402
|
-
|
|
99408
|
+
organization: result.organization ?? { id: result.orgId },
|
|
99403
99409
|
orgId: result.orgId,
|
|
99404
99410
|
projects: result.projects.map(({ id, name, slug }) => ({
|
|
99405
99411
|
id,
|
|
@@ -99407,7 +99413,7 @@ async function runMcpServer(options) {
|
|
|
99407
99413
|
slug
|
|
99408
99414
|
})),
|
|
99409
99415
|
...result.projects.length === 0 ? {
|
|
99410
|
-
note: "This
|
|
99416
|
+
note: "This organization has no projects. Call createProject, then connect to it."
|
|
99411
99417
|
} : {}
|
|
99412
99418
|
});
|
|
99413
99419
|
} catch (error) {
|
|
@@ -99421,14 +99427,14 @@ async function runMcpServer(options) {
|
|
|
99421
99427
|
const roleGatedTool = server.registerTool(
|
|
99422
99428
|
"createProject",
|
|
99423
99429
|
{
|
|
99424
|
-
description: "Create a new project in this session's
|
|
99430
|
+
description: "Create a new project in this session's organization. The new project becomes this session's target. Call connect to open a branch in it.",
|
|
99425
99431
|
inputSchema: {
|
|
99426
99432
|
name: z57.string().describe('Name for the new project, e.g. "Marketing Site"')
|
|
99427
99433
|
}
|
|
99428
99434
|
},
|
|
99429
99435
|
async ({ name }) => {
|
|
99430
99436
|
try {
|
|
99431
|
-
const executed = await
|
|
99437
|
+
const executed = await runOrganizationVerb("createProject", {
|
|
99432
99438
|
name,
|
|
99433
99439
|
kind: "app"
|
|
99434
99440
|
});
|
|
@@ -99438,7 +99444,7 @@ async function runMcpServer(options) {
|
|
|
99438
99444
|
);
|
|
99439
99445
|
}
|
|
99440
99446
|
const { project: created } = executed.data;
|
|
99441
|
-
await
|
|
99447
|
+
await teardownOrganizationClient();
|
|
99442
99448
|
return jsonResult({
|
|
99443
99449
|
id: created.id,
|
|
99444
99450
|
name: created.name,
|
|
@@ -99480,7 +99486,7 @@ async function runMcpServer(options) {
|
|
|
99480
99486
|
credentialBaseUrl: who.baseUrl,
|
|
99481
99487
|
credentialExpired: health.expired,
|
|
99482
99488
|
credentialExpiresInSeconds: health.expiresInSeconds,
|
|
99483
|
-
...
|
|
99489
|
+
...describeSelectedOrganization(),
|
|
99484
99490
|
...session ? {
|
|
99485
99491
|
session: {
|
|
99486
99492
|
projectId: session.projectId,
|
|
@@ -99520,8 +99526,8 @@ async function runMcpServer(options) {
|
|
|
99520
99526
|
await teardownSession();
|
|
99521
99527
|
sessionEndedBy = `signing out removed ${outcome.removedPath}, the credential this session's token was minted from`;
|
|
99522
99528
|
}
|
|
99523
|
-
|
|
99524
|
-
|
|
99529
|
+
selectedOrganization = null;
|
|
99530
|
+
ambientOrganization = null;
|
|
99525
99531
|
return jsonResult({
|
|
99526
99532
|
removed: true,
|
|
99527
99533
|
removedPath: outcome.removedPath,
|
|
@@ -99602,7 +99608,7 @@ async function runMcpServer(options) {
|
|
|
99602
99608
|
try {
|
|
99603
99609
|
if (s5.role === "read") {
|
|
99604
99610
|
throw new Error(
|
|
99605
|
-
"this
|
|
99611
|
+
"this organization role is read-only. NOTHING was written."
|
|
99606
99612
|
);
|
|
99607
99613
|
}
|
|
99608
99614
|
if (await awaitLiveSocket(s5) === "down") {
|
|
@@ -99762,7 +99768,7 @@ async function runMcpServer(options) {
|
|
|
99762
99768
|
const roleGatedTool = server.registerTool(
|
|
99763
99769
|
"updateProject",
|
|
99764
99770
|
{
|
|
99765
|
-
description: "Rename a project in this session's
|
|
99771
|
+
description: "Rename a project in this session's organization. Name the project by id or by its exact current name. This tool only changes the name.",
|
|
99766
99772
|
inputSchema: {
|
|
99767
99773
|
projectRef: z57.string().describe(
|
|
99768
99774
|
"The project to rename: its id, or its exact current name. Call listProjects to see what is there."
|
|
@@ -99772,7 +99778,7 @@ async function runMcpServer(options) {
|
|
|
99772
99778
|
},
|
|
99773
99779
|
async ({ projectRef, name }) => {
|
|
99774
99780
|
try {
|
|
99775
|
-
const executed = await
|
|
99781
|
+
const executed = await runOrganizationVerb("updateProject", {
|
|
99776
99782
|
projectRef,
|
|
99777
99783
|
name
|
|
99778
99784
|
});
|
|
@@ -99854,7 +99860,7 @@ async function runMcpServer(options) {
|
|
|
99854
99860
|
try {
|
|
99855
99861
|
if (s5.role === "read") {
|
|
99856
99862
|
throw new Error(
|
|
99857
|
-
"this
|
|
99863
|
+
"this organization role is read-only. NOTHING was written."
|
|
99858
99864
|
);
|
|
99859
99865
|
}
|
|
99860
99866
|
if (await awaitLiveSocket(s5) === "down") {
|
|
@@ -100023,7 +100029,7 @@ async function runMcpServer(options) {
|
|
|
100023
100029
|
// that move together, so it read `false` while a live localhost
|
|
100024
100030
|
// session was addressing production.
|
|
100025
100031
|
sessionInstance: session.baseUrl,
|
|
100026
|
-
|
|
100032
|
+
organization: describeWorkingIn(session.orgId),
|
|
100027
100033
|
baseUrlMismatch: stripSlash(baseUrl) !== session.baseUrl,
|
|
100028
100034
|
...moved ? { instanceMovedWarning: moved } : {},
|
|
100029
100035
|
bufferedEvents: session.events.length,
|
|
@@ -100037,9 +100043,9 @@ async function runMcpServer(options) {
|
|
|
100037
100043
|
credentialExpiringSoon: health.expiringSoon,
|
|
100038
100044
|
cliTokenExpiresAt,
|
|
100039
100045
|
cliTokenExpiresInSeconds: health.expiresInSeconds,
|
|
100040
|
-
// The
|
|
100046
|
+
// The organization pin survives a disconnect: it is a session
|
|
100041
100047
|
// choice, not a property of the socket.
|
|
100042
|
-
...
|
|
100048
|
+
...describeSelectedOrganization(),
|
|
100043
100049
|
baseUrl,
|
|
100044
100050
|
credentialBaseUrl: creds.baseUrl,
|
|
100045
100051
|
// A real comparison, not a constant. This was hard-coded `false`
|
|
@@ -100130,7 +100136,7 @@ async function runMcpServer(options) {
|
|
|
100130
100136
|
if (def3.mutates) trackRoleGatedTool(tool);
|
|
100131
100137
|
}
|
|
100132
100138
|
for (const def3 of OPAQUE_ACTIONS) {
|
|
100133
|
-
if (
|
|
100139
|
+
if (isOrganizationActionType(def3.type)) continue;
|
|
100134
100140
|
if (SESSION_WRAPPED_VERBS.has(def3.type)) continue;
|
|
100135
100141
|
if (!canServeCatalogAction(def3, options.readOnly === true)) continue;
|
|
100136
100142
|
registerCatalogVerb(def3);
|
|
@@ -100548,7 +100554,7 @@ init_login();
|
|
|
100548
100554
|
init_logout();
|
|
100549
100555
|
import { Command } from "commander";
|
|
100550
100556
|
|
|
100551
|
-
// src/commands/
|
|
100557
|
+
// src/commands/organization.ts
|
|
100552
100558
|
init_client();
|
|
100553
100559
|
init_session();
|
|
100554
100560
|
async function client(opts) {
|
|
@@ -100561,27 +100567,27 @@ async function client(opts) {
|
|
|
100561
100567
|
found.creds.baseUrl ?? opts.baseUrl
|
|
100562
100568
|
);
|
|
100563
100569
|
}
|
|
100564
|
-
async function
|
|
100565
|
-
const { organizations } = await (await client(opts)).
|
|
100570
|
+
async function runOrganizationList(opts) {
|
|
100571
|
+
const { organizations } = await (await client(opts)).listOrganizations();
|
|
100566
100572
|
if (organizations.length === 0) {
|
|
100567
100573
|
console.log(
|
|
100568
|
-
"No
|
|
100574
|
+
"No organizations. Create one with `zoah organization create <name>`."
|
|
100569
100575
|
);
|
|
100570
100576
|
return;
|
|
100571
100577
|
}
|
|
100572
|
-
for (const
|
|
100578
|
+
for (const organization of organizations) {
|
|
100573
100579
|
console.log(
|
|
100574
|
-
`${
|
|
100580
|
+
`${organization.active ? "*" : " "} ${organization.slug ?? organization.id} ${organization.name}`
|
|
100575
100581
|
);
|
|
100576
100582
|
}
|
|
100577
|
-
console.log("\n* = the
|
|
100583
|
+
console.log("\n* = the organization a command with no organization uses.");
|
|
100578
100584
|
}
|
|
100579
|
-
async function
|
|
100580
|
-
const created = await (await client(opts)).
|
|
100581
|
-
console.log(`Created
|
|
100582
|
-
if (created.
|
|
100585
|
+
async function runOrganizationCreate(opts) {
|
|
100586
|
+
const created = await (await client(opts)).createOrganization(opts.name);
|
|
100587
|
+
console.log(`Created organization "${created.name}" (${created.slug}).`);
|
|
100588
|
+
if (created.becameActiveOrganization) {
|
|
100583
100589
|
console.log(
|
|
100584
|
-
"This account had no
|
|
100590
|
+
"This account had no organization, so this one is now its default."
|
|
100585
100591
|
);
|
|
100586
100592
|
}
|
|
100587
100593
|
console.log(
|
|
@@ -101594,12 +101600,12 @@ async function main() {
|
|
|
101594
101600
|
fail(err);
|
|
101595
101601
|
}
|
|
101596
101602
|
});
|
|
101597
|
-
const
|
|
101598
|
-
"
|
|
101603
|
+
const organization = program.command("organization").description(
|
|
101604
|
+
"Organizations: the outermost scope, holding this account's projects."
|
|
101599
101605
|
);
|
|
101600
|
-
|
|
101606
|
+
organization.command("list").description("List the organizations this account belongs to.").action(async () => {
|
|
101601
101607
|
try {
|
|
101602
|
-
await
|
|
101608
|
+
await runOrganizationList({
|
|
101603
101609
|
cwd: process.cwd(),
|
|
101604
101610
|
baseUrl: program.opts().baseUrl ?? DEFAULT_BASE_URL
|
|
101605
101611
|
});
|
|
@@ -101607,9 +101613,9 @@ async function main() {
|
|
|
101607
101613
|
fail(err);
|
|
101608
101614
|
}
|
|
101609
101615
|
});
|
|
101610
|
-
|
|
101616
|
+
organization.command("create").argument("<name>", 'Organization name, e.g. "Acme Design"').description("Create an organization. The slug is derived from the name.").action(async (name) => {
|
|
101611
101617
|
try {
|
|
101612
|
-
await
|
|
101618
|
+
await runOrganizationCreate({
|
|
101613
101619
|
name,
|
|
101614
101620
|
cwd: process.cwd(),
|
|
101615
101621
|
baseUrl: program.opts().baseUrl ?? DEFAULT_BASE_URL
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoahhq/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Zoah CLI: import React components, publish via Zoah, swap local imports",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Zoah Inc.",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"tsup": "^8.3.5",
|
|
42
42
|
"typescript": "^5.8.3",
|
|
43
43
|
"vitest": "^2.1.8",
|
|
44
|
-
"@opacityapp/
|
|
45
|
-
"@opacityapp/
|
|
44
|
+
"@opacityapp/shared": "0.1.0",
|
|
45
|
+
"@opacityapp/opaque": "0.0.1"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsup",
|