overlord-cli 0.2606011152.0 → 0.2606021930.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/_cli/add-cwd.mjs +8 -16
- package/bin/_cli/attach.mjs +11 -23
- package/bin/_cli/auth.mjs +66 -17
- package/bin/_cli/credentials.mjs +130 -12
- package/bin/_cli/index.mjs +7 -0
- package/bin/_cli/new-ticket.mjs +59 -4
- package/bin/_cli/onboard.mjs +245 -0
- package/bin/_cli/protocol.mjs +24 -13
- package/bin/_cli/tickets.mjs +12 -27
- package/package.json +1 -1
- package/plugins/antigravity/scripts/overlord-mcp.mjs +6 -0
- package/plugins/antigravity/scripts/user-prompt-submit-hook.sh +23 -1
- package/plugins/antigravity/skills/overlord-ticket/SKILL.md +2 -2
- package/plugins/claude/scripts/user-prompt-submit-hook.sh +23 -1
- package/plugins/claude/skills/overlord-ticket/SKILL.md +2 -2
- package/plugins/claude/skills/overlord-ticket/reference/cli.md +1 -1
- package/plugins/claude/skills/overlord-ticket/reference/devices.md +1 -1
- package/plugins/cursor/hooks/overlord-user-prompt-submit.sh +23 -1
- package/plugins/cursor/skills/overlord-ticket/SKILL.md +2 -2
- package/plugins/cursor/skills/overlord-ticket/reference/cli.md +1 -1
- package/plugins/cursor/skills/overlord-ticket/reference/devices.md +1 -1
- package/plugins/overlord/scripts/overlord-mcp.mjs +9 -1
- package/plugins/overlord/scripts/user-prompt-submit-hook.sh +34 -1
- package/plugins/overlord/skills/overlord-ticket/SKILL.md +2 -2
- package/plugins/overlord/skills/overlord-ticket/reference/cli.md +1 -1
- package/plugins/overlord/skills/overlord-ticket/reference/devices.md +1 -1
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ ovld doctor
|
|
|
89
89
|
|
|
90
90
|
**Antigravity:** Gemini CLI is deprecated. Use `ovld setup antigravity` and `ovld launch antigravity --ticket-id <ticket_id>`. Antigravity manages model selection internally.
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
The CLI is organization-agnostic: `ovld auth login` stores your identity only and never a default organization. Your identity (OAuth session or agent token) resolves to the list of organizations you belong to — see `ovld auth status --verbose`. Each command resolves its organization from the `ticket_id` (values such as `1:899` carry the organization id), then an explicit `--organization-id`, otherwise from your membership. Commands that browse or search — `ovld add-cwd`, `ovld create`/`ovld prompt`, `ovld tickets list`, and `ovld protocol search-tickets` — span every organization you belong to. Actions against an organization you are not a member of return a clear permission error rather than silently falling back to another org.
|
|
93
93
|
|
|
94
94
|
When `ovld launch` has an explicit `--working-directory`, or when you run it from a registered project directory containing `.overlord/project.json`, it exports `TMPDIR`, `TMP`, `TEMP`, and `OVERLORD_TMPDIR` to that project's `.overlord/tmp/` directory before spawning the agent. Otherwise it falls back to the system temp directory.
|
|
95
95
|
|
|
@@ -141,7 +141,7 @@ Agents can find docs here: https://www.ovld.ai/docs/for-agents
|
|
|
141
141
|
- `record-change-rationales` - persist structured change rationales without a normal progress update
|
|
142
142
|
- `ask` - post a blocking question and move the ticket to review
|
|
143
143
|
- `permission-request` - notify Overlord that the agent is requesting tool permission
|
|
144
|
-
- `hook-event` - record lifecycle hook events such as `UserPromptSubmit` / Cursor `beforeSubmitPrompt` without a session key
|
|
144
|
+
- `hook-event` - record lifecycle hook events such as `UserPromptSubmit` / Cursor `beforeSubmitPrompt` without a session key; optionally persist a native resume/session id onto the active Overlord session
|
|
145
145
|
- `read-context` - read shared persistent context for this ticket
|
|
146
146
|
- `write-context` - write shared persistent context for future sessions
|
|
147
147
|
- `deliver` - send artifacts/rationales and move the ticket to review
|
package/bin/_cli/add-cwd.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { stdin as input, stdout as output } from 'node:process';
|
|
|
12
12
|
|
|
13
13
|
import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
|
|
14
14
|
import { upsertLocalOverlordConfig } from './local-config.mjs';
|
|
15
|
-
import {
|
|
15
|
+
import { fetchProjectsAcrossOrganizations, parseNumberedSelection } from './new-ticket.mjs';
|
|
16
16
|
import { readOrCreateDeviceFingerprint } from './runner.mjs';
|
|
17
17
|
|
|
18
18
|
function parseFlags(args) {
|
|
@@ -41,19 +41,6 @@ function projectLabel(project) {
|
|
|
41
41
|
return organizationName ? `${project.name} — ${organizationName}` : project.name;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async function fetchProjects(platformUrl, bearerToken, localSecret, organizationId) {
|
|
45
|
-
const res = await fetch(`${platformUrl}/api/protocol/projects`, {
|
|
46
|
-
headers: buildAuthHeaders(bearerToken, localSecret, organizationId)
|
|
47
|
-
});
|
|
48
|
-
const data = await res.json().catch(() => ({}));
|
|
49
|
-
if (!res.ok) {
|
|
50
|
-
throw new Error(
|
|
51
|
-
`Failed to list projects (${res.status}): ${data.error ?? JSON.stringify(data)}`
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
return Array.isArray(data.projects) ? sortProjects(data.projects) : [];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
44
|
async function promptForProject(projects, cwd) {
|
|
58
45
|
if (!projects.length) {
|
|
59
46
|
throw new Error('No projects available. Create a project in Overlord first.');
|
|
@@ -118,9 +105,10 @@ By default the registered resource is marked primary for this device. Pass
|
|
|
118
105
|
|
|
119
106
|
const isPrimary = !(flags.primary === 'false' || flags.primary === false);
|
|
120
107
|
|
|
121
|
-
const
|
|
108
|
+
const auth = await resolveAuth({});
|
|
109
|
+
const { platformUrl, bearerToken, localSecret } = auth;
|
|
122
110
|
|
|
123
|
-
const projects = await
|
|
111
|
+
const projects = await fetchProjectsAcrossOrganizations(auth);
|
|
124
112
|
let project = null;
|
|
125
113
|
if (typeof flags['project-id'] === 'string' && flags['project-id'].trim()) {
|
|
126
114
|
const wanted = flags['project-id'].trim();
|
|
@@ -139,6 +127,10 @@ By default the registered resource is marked primary for this device. Pass
|
|
|
139
127
|
|
|
140
128
|
const deviceFingerprint = readOrCreateDeviceFingerprint(flags);
|
|
141
129
|
|
|
130
|
+
// Register the resource in the organization that owns the chosen project,
|
|
131
|
+
// never a stored default org.
|
|
132
|
+
const organizationId = project.organizationId ?? null;
|
|
133
|
+
|
|
142
134
|
const body = {
|
|
143
135
|
projectId: project.id,
|
|
144
136
|
directoryPath,
|
package/bin/_cli/attach.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* ovld attach <ticketId> <agent> # non-interactive: launch immediately
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { resolveAuth, searchTicketsAcrossOrganizations } from './credentials.mjs';
|
|
13
13
|
import { runLauncherCommand } from './launcher.mjs';
|
|
14
14
|
|
|
15
15
|
const AGENTS = ['claude', 'cursor', 'codex', 'antigravity', 'opencode'];
|
|
@@ -58,26 +58,15 @@ function statusColor(status) {
|
|
|
58
58
|
|
|
59
59
|
// ─── API ──────────────────────────────────────────────────────────────────────
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
body: JSON.stringify({
|
|
69
|
-
includeCompleted: false,
|
|
70
|
-
query,
|
|
71
|
-
limit: MAX_VISIBLE
|
|
72
|
-
})
|
|
61
|
+
// Organization-agnostic ticket search: spans every organization the identity
|
|
62
|
+
// belongs to (no default org) by delegating to the shared fan-out helper.
|
|
63
|
+
async function searchTickets(auth, query) {
|
|
64
|
+
const { tickets } = await searchTicketsAcrossOrganizations(auth, {
|
|
65
|
+
includeCompleted: false,
|
|
66
|
+
query,
|
|
67
|
+
limit: MAX_VISIBLE
|
|
73
68
|
});
|
|
74
|
-
|
|
75
|
-
if (!res.ok) {
|
|
76
|
-
throw new Error(`Failed to search tickets (${res.status}): ${await res.text()}`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const data = await res.json();
|
|
80
|
-
return data.tickets ?? [];
|
|
69
|
+
return tickets ?? [];
|
|
81
70
|
}
|
|
82
71
|
|
|
83
72
|
// ─── Interactive prompt ───────────────────────────────────────────────────────
|
|
@@ -287,7 +276,7 @@ function runInteractivePrompt({ label, items = [], search, prefix = '' }) {
|
|
|
287
276
|
export async function runAttachCommand(args) {
|
|
288
277
|
const [ticketIdArg, agentArg] = args;
|
|
289
278
|
|
|
290
|
-
const
|
|
279
|
+
const auth = await resolveAuth();
|
|
291
280
|
|
|
292
281
|
// ── Phase 1: Ticket selection ──────────────────────────────────────────────
|
|
293
282
|
|
|
@@ -301,8 +290,7 @@ export async function runAttachCommand(args) {
|
|
|
301
290
|
|
|
302
291
|
const selectedTicket = await runInteractivePrompt({
|
|
303
292
|
label: 'Search tickets',
|
|
304
|
-
search: nextQuery =>
|
|
305
|
-
searchTickets(platformUrl, bearerToken, localSecret, organizationId, nextQuery)
|
|
293
|
+
search: nextQuery => searchTickets(auth, nextQuery)
|
|
306
294
|
});
|
|
307
295
|
|
|
308
296
|
if (!selectedTicket) {
|
package/bin/_cli/auth.mjs
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
loadCredentials,
|
|
14
14
|
loadRuntime,
|
|
15
15
|
repairCredentials,
|
|
16
|
+
resolveAuth,
|
|
17
|
+
resolveOrganizations,
|
|
16
18
|
saveCredentials
|
|
17
19
|
} from './credentials.mjs';
|
|
18
20
|
|
|
@@ -558,47 +560,91 @@ export async function authLogin(args = []) {
|
|
|
558
560
|
credentials.access_token,
|
|
559
561
|
localSecret
|
|
560
562
|
);
|
|
561
|
-
|
|
563
|
+
|
|
564
|
+
// The CLI is organization-agnostic: login stores the identity only, never a
|
|
565
|
+
// default organization. `--organization-id` is still validated so a typo is
|
|
566
|
+
// caught early, but it is no longer persisted — every command resolves its org
|
|
567
|
+
// from the ticket id, an explicit --organization-id, or your membership.
|
|
568
|
+
if (preselectedOrganizationId !== null) {
|
|
569
|
+
selectLoginOrganization(organizations, preselectedOrganizationId);
|
|
570
|
+
}
|
|
562
571
|
|
|
563
572
|
saveCredentials({
|
|
564
573
|
access_token: credentials.access_token,
|
|
565
574
|
access_token_expires_at: credentials.access_token_expires_at ?? undefined,
|
|
566
575
|
refresh_token: credentials.refresh_token,
|
|
567
|
-
organization_id: selectedOrganization.id,
|
|
568
576
|
platform_url: resolvedPlatformUrl
|
|
569
577
|
});
|
|
570
578
|
|
|
571
|
-
|
|
579
|
+
console.log('Logged in successfully.');
|
|
580
|
+
printOrganizationMemberships(organizations);
|
|
581
|
+
if (preselectedOrganizationId !== null) {
|
|
572
582
|
console.log(
|
|
573
|
-
|
|
574
|
-
'
|
|
583
|
+
`\nNote: --organization-id is no longer stored as a default. ` +
|
|
584
|
+
'Commands resolve their organization from the ticket id, an explicit --organization-id, or your membership.'
|
|
575
585
|
);
|
|
576
|
-
} else {
|
|
577
|
-
console.log(`Logged in successfully. Default organization: ${describeOrganization(selectedOrganization)}.`);
|
|
578
586
|
}
|
|
579
587
|
}
|
|
580
588
|
|
|
589
|
+
function printOrganizationMemberships(organizations) {
|
|
590
|
+
if (!organizations.length) {
|
|
591
|
+
console.log(' You are not yet a member of any organization. Complete onboarding in Overlord first.');
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (organizations.length === 1) {
|
|
596
|
+
console.log(` Organization: ${describeOrganization(organizations[0])}`);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
console.log(` Member of ${organizations.length} organizations:`);
|
|
601
|
+
for (const organization of organizations) {
|
|
602
|
+
console.log(` - ${describeOrganization(organization)}`);
|
|
603
|
+
}
|
|
604
|
+
console.log(' Commands resolve their organization from the ticket id or --organization-id.');
|
|
605
|
+
}
|
|
606
|
+
|
|
581
607
|
async function authLoginWithAgentToken(agentToken, args) {
|
|
582
608
|
const preselectedOrganizationId = parseOrganizationFlag(args);
|
|
583
609
|
const storedCredentials = loadCredentials();
|
|
584
610
|
const platformUrl = resolveLoginPlatformUrl(null, storedCredentials?.platform_url ?? null);
|
|
585
611
|
|
|
612
|
+
// Agent-token login stores the identity only — never a default organization.
|
|
586
613
|
saveCredentials({
|
|
587
614
|
agent_token: agentToken,
|
|
588
|
-
platform_url: platformUrl
|
|
589
|
-
...(preselectedOrganizationId !== null ? { organization_id: preselectedOrganizationId } : {})
|
|
615
|
+
platform_url: platformUrl
|
|
590
616
|
});
|
|
591
617
|
|
|
592
618
|
console.log(`Agent token saved. The CLI will use this token for all protocol commands.`);
|
|
619
|
+
console.log(` Organizations are resolved from your token's membership per command.`);
|
|
593
620
|
if (preselectedOrganizationId !== null) {
|
|
594
|
-
console.log(
|
|
595
|
-
|
|
596
|
-
|
|
621
|
+
console.log(
|
|
622
|
+
` Note: --organization-id is no longer stored as a default; pass it per command (or use a ticket id) to scope an action.`
|
|
623
|
+
);
|
|
597
624
|
}
|
|
598
625
|
console.log(` Platform URL: ${platformUrl}`);
|
|
599
626
|
console.log(`\nTo remove: ovld auth logout`);
|
|
600
627
|
}
|
|
601
628
|
|
|
629
|
+
async function printOrganizationMembershipStatus() {
|
|
630
|
+
try {
|
|
631
|
+
const auth = await resolveAuth();
|
|
632
|
+
const organizations = await resolveOrganizations(auth);
|
|
633
|
+
if (!organizations.length) {
|
|
634
|
+
console.log(' Organizations: none (not a member of any organization)');
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
console.log(` Organizations (${organizations.length}):`);
|
|
638
|
+
for (const organization of organizations) {
|
|
639
|
+
console.log(` - ${describeOrganization(organization)}`);
|
|
640
|
+
}
|
|
641
|
+
} catch (error) {
|
|
642
|
+
console.log(
|
|
643
|
+
` Organizations: unavailable (${error instanceof Error ? error.message : String(error)})`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
602
648
|
async function printVerboseAuthStatus() {
|
|
603
649
|
const status = await getAuthStatus();
|
|
604
650
|
if (!status.isLoggedIn) {
|
|
@@ -617,7 +663,9 @@ async function printVerboseAuthStatus() {
|
|
|
617
663
|
console.log(` Token source: ${status.tokenSource}`);
|
|
618
664
|
console.log(` Token present: ${status.tokenPresent ? 'yes' : 'no'}`);
|
|
619
665
|
console.log(` Auth mode: ${status.authMode}`);
|
|
620
|
-
|
|
666
|
+
if (status.isLoggedIn) {
|
|
667
|
+
await printOrganizationMembershipStatus();
|
|
668
|
+
}
|
|
621
669
|
if (status.error) {
|
|
622
670
|
console.log(` Error: ${status.error}`);
|
|
623
671
|
}
|
|
@@ -681,10 +729,11 @@ Subcommands:
|
|
|
681
729
|
login Authorize the CLI via browser (works locally or over SSH)
|
|
682
730
|
--token <oat_…> Persist an agent token from Settings → Agents & MCP.
|
|
683
731
|
Skips the browser flow; token never expires.
|
|
684
|
-
--organization-id <id> Optional
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
732
|
+
--organization-id <id> Optional. Validated against your membership but
|
|
733
|
+
no longer stored as a default — the CLI is
|
|
734
|
+
organization-agnostic. Scope a command with a
|
|
735
|
+
ticket id (e.g. 1:899) or --organization-id.
|
|
736
|
+
status Show current login status (use --verbose to list your organizations)
|
|
688
737
|
repair Mirror and chmod shared Desktop/CLI credentials when possible
|
|
689
738
|
logout Remove stored credentials
|
|
690
739
|
`);
|
package/bin/_cli/credentials.mjs
CHANGED
|
@@ -584,12 +584,12 @@ export async function resolveAuth(options = {}) {
|
|
|
584
584
|
typeof process.env.OVERLORD_ORGANIZATION_ID === 'string'
|
|
585
585
|
? Number.parseInt(process.env.OVERLORD_ORGANIZATION_ID, 10)
|
|
586
586
|
: null;
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
587
|
+
// Organization is no longer required up front. When neither a per-action hint
|
|
588
|
+
// nor an explicit OVERLORD_ORGANIZATION_ID override is present, the platform
|
|
589
|
+
// resolves the organization from the token owner's membership, so the CLI stays
|
|
590
|
+
// org-agnostic instead of pinning a single default.
|
|
591
|
+
const resolvedOrganizationId =
|
|
592
|
+
organizationIdHint ?? (Number.isFinite(envOrganizationId) ? envOrganizationId : null);
|
|
593
593
|
|
|
594
594
|
if (isEnvAccessTokenUsable(envAccessToken)) {
|
|
595
595
|
return {
|
|
@@ -603,11 +603,14 @@ export async function resolveAuth(options = {}) {
|
|
|
603
603
|
}
|
|
604
604
|
|
|
605
605
|
if (creds?.agent_token) {
|
|
606
|
+
// Stored credentials never act as a default organization. Org is taken from the
|
|
607
|
+
// per-action hint (ticket id or --organization-id) and otherwise left to the
|
|
608
|
+
// platform's membership lookup.
|
|
606
609
|
return {
|
|
607
610
|
platformUrl,
|
|
608
611
|
bearerToken: creds.agent_token,
|
|
609
612
|
localSecret,
|
|
610
|
-
organizationId: organizationIdHint ??
|
|
613
|
+
organizationId: organizationIdHint ?? null,
|
|
611
614
|
authMode: 'agent_token'
|
|
612
615
|
};
|
|
613
616
|
}
|
|
@@ -641,19 +644,18 @@ export async function resolveAuth(options = {}) {
|
|
|
641
644
|
}
|
|
642
645
|
}
|
|
643
646
|
|
|
644
|
-
if (!Number.isFinite(nextCredentials.organization_id)) {
|
|
645
|
-
throw new Error('Overlord login is missing an organization selection. Run `ovld auth login` again.');
|
|
646
|
-
}
|
|
647
|
-
|
|
648
647
|
if (!nextCredentials.access_token) {
|
|
649
648
|
throw new Error('No OAuth access token is available. Run `ovld auth login` again.');
|
|
650
649
|
}
|
|
651
650
|
|
|
651
|
+
// OAuth sessions are not bound to a stored default organization. The platform
|
|
652
|
+
// derives the organization from the signed-in user's membership when no
|
|
653
|
+
// per-action hint is supplied, keeping the CLI org-agnostic.
|
|
652
654
|
return {
|
|
653
655
|
platformUrl,
|
|
654
656
|
bearerToken: nextCredentials.access_token,
|
|
655
657
|
localSecret,
|
|
656
|
-
organizationId: organizationIdHint ??
|
|
658
|
+
organizationId: organizationIdHint ?? null,
|
|
657
659
|
authMode: 'oauth'
|
|
658
660
|
};
|
|
659
661
|
}
|
|
@@ -667,6 +669,122 @@ export async function resolveAuth(options = {}) {
|
|
|
667
669
|
};
|
|
668
670
|
}
|
|
669
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Lists every organization the authenticated identity belongs to.
|
|
674
|
+
*
|
|
675
|
+
* This is the org-agnostic replacement for a stored "default organization": the
|
|
676
|
+
* caller's identity (OAuth session or agent token) resolves to the full set of
|
|
677
|
+
* memberships, and commands fan out or prompt over that list instead of assuming
|
|
678
|
+
* a single org. Works for OAuth, agent tokens, and local-dev alike because
|
|
679
|
+
* `/api/protocol/organizations` resolves the caller through the standard
|
|
680
|
+
* protocol auth path.
|
|
681
|
+
*
|
|
682
|
+
* @param {{ platformUrl: string, bearerToken: string, localSecret?: string }} auth
|
|
683
|
+
* @returns {Promise<Array<{ id: number, name: string }>>}
|
|
684
|
+
*/
|
|
685
|
+
export async function resolveOrganizations(auth) {
|
|
686
|
+
const { platformUrl, bearerToken, localSecret } = auth;
|
|
687
|
+
let res;
|
|
688
|
+
try {
|
|
689
|
+
res = await fetch(`${platformUrl}/api/protocol/organizations`, {
|
|
690
|
+
method: 'POST',
|
|
691
|
+
headers: {
|
|
692
|
+
...buildAuthHeaders(bearerToken, localSecret),
|
|
693
|
+
'Content-Type': 'application/json'
|
|
694
|
+
},
|
|
695
|
+
body: JSON.stringify({})
|
|
696
|
+
});
|
|
697
|
+
} catch (error) {
|
|
698
|
+
throw describeNetworkError(error, `Failed to list organizations from ${platformUrl}`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const data = await res.json().catch(() => ({}));
|
|
702
|
+
if (!res.ok) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
`Failed to list organizations (${res.status}): ${data.error ?? JSON.stringify(data)}`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return Array.isArray(data.organizations)
|
|
709
|
+
? data.organizations
|
|
710
|
+
.map(org => ({ id: org.id, name: typeof org.name === 'string' ? org.name : '' }))
|
|
711
|
+
.filter(org => Number.isFinite(org.id))
|
|
712
|
+
: [];
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Searches tickets across every organization the identity belongs to.
|
|
717
|
+
*
|
|
718
|
+
* Organization-agnostic by default: with no explicit organization it resolves the
|
|
719
|
+
* membership list and fans out the per-org search, merging and de-duplicating by
|
|
720
|
+
* ticket id. An explicit `organizationId` (from --organization-id) scopes the
|
|
721
|
+
* search to that single org instead. There is no default-org fallback.
|
|
722
|
+
*
|
|
723
|
+
* @param {{ platformUrl: string, bearerToken: string, localSecret?: string }} auth
|
|
724
|
+
* @param {Record<string, unknown>} body search-tickets request body
|
|
725
|
+
* @param {{ organizationId?: number | null }} [options]
|
|
726
|
+
* @returns {Promise<{ tickets: unknown[], count: number }>}
|
|
727
|
+
*/
|
|
728
|
+
export async function searchTicketsAcrossOrganizations(auth, body, options = {}) {
|
|
729
|
+
const { platformUrl, bearerToken, localSecret } = auth;
|
|
730
|
+
const explicitOrganizationId =
|
|
731
|
+
Number.isFinite(options.organizationId) ? options.organizationId : null;
|
|
732
|
+
|
|
733
|
+
const post = async organizationId => {
|
|
734
|
+
const res = await fetch(`${platformUrl}/api/protocol/search-tickets`, {
|
|
735
|
+
method: 'POST',
|
|
736
|
+
headers: {
|
|
737
|
+
...buildAuthHeaders(bearerToken, localSecret, organizationId),
|
|
738
|
+
'Content-Type': 'application/json'
|
|
739
|
+
},
|
|
740
|
+
body: JSON.stringify(body)
|
|
741
|
+
});
|
|
742
|
+
const data = await res.json().catch(() => ({}));
|
|
743
|
+
if (!res.ok) {
|
|
744
|
+
throw new Error(
|
|
745
|
+
`Failed to search tickets (${res.status}): ${data.error ?? JSON.stringify(data)}`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
return Array.isArray(data.tickets) ? data.tickets : [];
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
if (explicitOrganizationId !== null) {
|
|
752
|
+
const tickets = await post(explicitOrganizationId);
|
|
753
|
+
return { tickets, count: tickets.length };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const organizations = await resolveOrganizations(auth);
|
|
757
|
+
if (!organizations.length) {
|
|
758
|
+
const tickets = await post(null);
|
|
759
|
+
return { tickets, count: tickets.length };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const results = await Promise.allSettled(organizations.map(org => post(org.id)));
|
|
763
|
+
const failures = results
|
|
764
|
+
.map((result, index) => ({ result, organization: organizations[index] }))
|
|
765
|
+
.filter(({ result }) => result.status === 'rejected');
|
|
766
|
+
if (failures.length > 0) {
|
|
767
|
+
const detail = failures
|
|
768
|
+
.map(({ result, organization }) => {
|
|
769
|
+
const reason = result.status === 'rejected' ? result.reason : null;
|
|
770
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
771
|
+
return `${organization.name || `organization ${organization.id}`} (${organization.id}): ${message}`;
|
|
772
|
+
})
|
|
773
|
+
.join('; ');
|
|
774
|
+
throw new Error(`Ticket search returned partial failures: ${detail}`);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const perOrg = results.map(result => (result.status === 'fulfilled' ? result.value : []));
|
|
778
|
+
const byId = new Map();
|
|
779
|
+
for (const ticket of perOrg.flat()) {
|
|
780
|
+
if (ticket && ticket.id && !byId.has(ticket.id)) {
|
|
781
|
+
byId.set(ticket.id, ticket);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const tickets = [...byId.values()];
|
|
785
|
+
return { tickets, count: tickets.length };
|
|
786
|
+
}
|
|
787
|
+
|
|
670
788
|
export async function getAuthStatus() {
|
|
671
789
|
const creds = loadCredentials();
|
|
672
790
|
let resolved;
|
package/bin/_cli/index.mjs
CHANGED
|
@@ -34,6 +34,7 @@ Usage:
|
|
|
34
34
|
${primaryCommand} create "<objective>" Create a ticket with numbered project selection; supports --agent/--model/--delegate
|
|
35
35
|
${primaryCommand} prompt "<objective>" Create a ticket, then launch an agent on it
|
|
36
36
|
${primaryCommand} add-cwd Register the current directory as a project resource (interactive)
|
|
37
|
+
${primaryCommand} onboard Create an account, project, cwd link, and onboarding ticket
|
|
37
38
|
${primaryCommand} auth <subcommand> Login, logout, repair, or check auth status
|
|
38
39
|
${primaryCommand} tickets <subcommand> Create or list tickets
|
|
39
40
|
${primaryCommand} ticket <subcommand> Work with a single ticket
|
|
@@ -117,6 +118,12 @@ export async function runCli({ primaryCommand }) {
|
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
if (command === 'onboard') {
|
|
122
|
+
const { runOnboardCommand } = await import('./onboard.mjs');
|
|
123
|
+
await runOnboardCommand(rest);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
// Auth group
|
|
121
128
|
if (command === 'auth') {
|
|
122
129
|
await runAuthCommand(rest[0], rest.slice(1));
|
package/bin/_cli/new-ticket.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import readline from 'node:readline';
|
|
|
4
4
|
import { stdin as input, stdout as output } from 'node:process';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
|
|
7
|
-
import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
|
|
7
|
+
import { buildAuthHeaders, resolveAuth, resolveOrganizations } from './credentials.mjs';
|
|
8
8
|
import { runLauncherCommand } from './launcher.mjs';
|
|
9
9
|
|
|
10
10
|
const PROMPT_AGENT_IDENTIFIERS = {
|
|
@@ -232,7 +232,57 @@ async function fetchProjects(platformUrl, bearerToken, localSecret, organization
|
|
|
232
232
|
);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
return Array.isArray(data.projects) ?
|
|
235
|
+
return Array.isArray(data.projects) ? data.projects : [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Lists projects across every organization the identity belongs to.
|
|
240
|
+
*
|
|
241
|
+
* The CLI is organization-agnostic: instead of listing only a single default
|
|
242
|
+
* org's projects, it resolves the membership list and fans out the per-org
|
|
243
|
+
* projects query, then merges and de-duplicates by project id. Each project
|
|
244
|
+
* carries its own `organizationId`, so the caller scopes follow-up writes (ticket
|
|
245
|
+
* creation, resource registration) to the chosen project's org — no default org.
|
|
246
|
+
*
|
|
247
|
+
* @param {{ platformUrl: string, bearerToken: string, localSecret?: string }} auth
|
|
248
|
+
*/
|
|
249
|
+
export async function fetchProjectsAcrossOrganizations(auth) {
|
|
250
|
+
const { platformUrl, bearerToken, localSecret } = auth;
|
|
251
|
+
const organizations = await resolveOrganizations(auth);
|
|
252
|
+
|
|
253
|
+
// No memberships resolved (or a backend that scopes by membership anyway): fall
|
|
254
|
+
// back to a single unscoped query so the command still works.
|
|
255
|
+
if (!organizations.length) {
|
|
256
|
+
return sortProjects(await fetchProjects(platformUrl, bearerToken, localSecret, null));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const results = await Promise.allSettled(
|
|
260
|
+
organizations.map(org => fetchProjects(platformUrl, bearerToken, localSecret, org.id))
|
|
261
|
+
);
|
|
262
|
+
const failures = results
|
|
263
|
+
.map((result, index) => ({ result, organization: organizations[index] }))
|
|
264
|
+
.filter(({ result }) => result.status === 'rejected');
|
|
265
|
+
if (failures.length > 0) {
|
|
266
|
+
const detail = failures
|
|
267
|
+
.map(({ result, organization }) => {
|
|
268
|
+
const reason = result.status === 'rejected' ? result.reason : null;
|
|
269
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
270
|
+
return `${organization.name || `organization ${organization.id}`} (${organization.id}): ${message}`;
|
|
271
|
+
})
|
|
272
|
+
.join('; ');
|
|
273
|
+
throw new Error(`Project listing returned partial failures: ${detail}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const perOrg = results.map(result => (result.status === 'fulfilled' ? result.value : []));
|
|
277
|
+
|
|
278
|
+
const byId = new Map();
|
|
279
|
+
for (const project of perOrg.flat()) {
|
|
280
|
+
if (project && project.id && !byId.has(project.id)) {
|
|
281
|
+
byId.set(project.id, project);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return sortProjects([...byId.values()]);
|
|
236
286
|
}
|
|
237
287
|
|
|
238
288
|
async function createTicket(platformUrl, bearerToken, localSecret, organizationId, body) {
|
|
@@ -312,8 +362,9 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
|
|
|
312
362
|
const { flags } = parseFlags(args);
|
|
313
363
|
const objectiveInput = await resolveObjectiveInput(flags);
|
|
314
364
|
|
|
315
|
-
const
|
|
316
|
-
const
|
|
365
|
+
const auth = await resolveAuth();
|
|
366
|
+
const { platformUrl, bearerToken, localSecret } = auth;
|
|
367
|
+
const projects = await fetchProjectsAcrossOrganizations(auth);
|
|
317
368
|
|
|
318
369
|
if (!projects.length) {
|
|
319
370
|
throw new Error('No projects available. Create a project first.');
|
|
@@ -328,6 +379,10 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
|
|
|
328
379
|
renderItem: project => projectLabel(project)
|
|
329
380
|
}));
|
|
330
381
|
|
|
382
|
+
// The ticket is created in the organization that owns the chosen project,
|
|
383
|
+
// never a stored default org.
|
|
384
|
+
const organizationId = selectedProject.organizationId ?? null;
|
|
385
|
+
|
|
331
386
|
const selectedAgent = launchAgent
|
|
332
387
|
? (resolveAgent(typeof flags.agent === 'string' ? flags.agent : '') ??
|
|
333
388
|
(await promptForSelection({
|