Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions internal/api/gen/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions internal/api/queries/team.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1400,3 +1400,15 @@ mutation TeamStopSignalLift($appRef: String!, $id: ID!) {
signal { ...StopSignalFields }
}
}

# Default-channel identity only, no transcript/cursor read or cursor mutation.
query TeamChannelLocation($appRef: ID!) {
app(ref: $appRef) {
id
urn
defaultChannel {
id
chatRootNodeId
}
}
}
14 changes: 14 additions & 0 deletions internal/cmd/agentic/agentic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -3460,3 +3460,17 @@ format delegates the provider default. No local output-file or remote document
creation option. Server delegates format/binary/size guarantees to its tool;
file_too_large and unknown failures retain existing error mapping/refusal text,
with no retry/reconnect guess. No hidden discovery, writes or uploads.

## Default team channel location (#489)

`hadron team channel [--app <ref>]` answers where the default team channel lives.
It uses the existing team App resolution (--app, configured context, binding),
reports that source, and copies the server-built URL verbatim. The destination
is the channel root node's own page and channel view, not an App conversation
selector. No route is composed. JSON is {app,appSource,channelId,portalUrl};
portalUrl is present and nullable. Text prints URL: only when supplied, otherwise
says no channel URL is available. One metadata read and one best-effort authorized
root node read, no transcript fetch or cursor/binding write. The root read records
a read and may decrypt internally. A concealed/missing/failed root read leaves
the URL null; initial lookup failures remain errors. Existing channel get/list
commands are unchanged.
85 changes: 85 additions & 0 deletions internal/cmd/team/channel_location.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package team

import (
"fmt"
"io"

"github.com/hadron-memory/hadron-cli/internal/api"
"github.com/hadron-memory/hadron-cli/internal/api/gen"
"github.com/hadron-memory/hadron-cli/internal/cmdutil"
"github.com/hadron-memory/hadron-cli/internal/exitcode"
"github.com/hadron-memory/hadron-cli/internal/output"
"github.com/spf13/cobra"
)

type teamChannelLocationDTO struct {
App string `json:"app"`
AppSource string `json:"appSource"`
ChannelID string `json:"channelId"`
PortalURL *string `json:"portalUrl"`
}

func newCmdTeamChannel(f *cmdutil.Factory) *cobra.Command {
return &cobra.Command{
Use: "channel", Short: "Show the default team channel's server-built URL",
Long: `Find the default team channel without knowing its id or address.

The team App resolves from --app, the configured App context, or the worktree
binding, just as other team commands do. Output names the App and scope source.
The server-built URL opens the channel root node's own page and channel view;
this does not select an App-level conversation or create a new portal route.

One default-channel metadata read is followed by one authorized root-node read
for identity and URL only. That node read records a read and may decrypt
internally; no transcript is fetched, no cursor is advanced, and no binding
is started or changed. No URL is composed by the client. If the server supplies
no link, or the optional root read fails or is concealed, portalUrl is null in
JSON and text says no channel URL is available. The default-channel lookup
itself must succeed.`,
Example: " hadron team channel\n hadron team channel --app acme.com:team --json",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
b, err := readBindingOrNilWithApp(ctx, f)
if err != nil {
return err
}
scope, err := resolveTeamAppScope(ctx, f, b)
if err != nil {
return err
}
client, err := f.GraphQLClient()
if err != nil {
return err
}
resp, err := gen.TeamChannelLocation(ctx, client, scope.Ref)
if err != nil {
return api.MapError(err)
}
if resp.App == nil {
return exitcode.Newf(exitcode.NotFound, "no team App is readable here")
}
if resp.App.DefaultChannel == nil {
return exitcode.Newf(exitcode.NotFound, "the App has no default team channel")
}
d := teamChannelLocationDTO{App: resp.App.Urn, AppSource: scope.Source, ChannelID: resp.App.DefaultChannel.Id}
if ref := resp.App.DefaultChannel.ChatRootNodeId; ref != "" {
pointer, pointerErr := gen.NodePortal(ctx, client, ref)
if pointerErr == nil && pointer != nil && pointer.Node != nil {
d.PortalURL = pointer.Node.PortalUrl
}
}
return output.Write(f.IOStreams, f.JSON, d, func(w io.Writer) error {
if _, err := fmt.Fprintf(w, "app: %s (%s)\nchannel: %s\n", d.App, d.AppSource, d.ChannelID); err != nil {
return err
}
if d.PortalURL != nil {
_, err := fmt.Fprintf(w, "URL: %s\n", *d.PortalURL)
return err
}
_, err := fmt.Fprintln(w, "no channel URL is available")
return err
})
},
}
}
1 change: 1 addition & 0 deletions internal/cmd/team/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ and ` + "`hadron app agent list <app>`" + ` (the installed cast pool).`,
cmd.AddCommand(newCmdRole(f))
cmd.AddCommand(newCmdSession(f))
cmd.AddCommand(newCmdTeamChat(f))
cmd.AddCommand(newCmdTeamChannel(f))
cmd.AddCommand(newCmdAttention(f))
cmd.AddCommand(newCmdTeamTask(f))
cmd.AddCommand(newCmdTeamRoster(f))
Expand Down
Loading
Loading