gipity 1.0.414 → 1.0.416

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.
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { get, post, put, del } from '../api.js';
3
- import { resolveProjectContext, saveConfig } from '../config.js';
3
+ import { resolveProjectContext, requireConfig, saveConfig } from '../config.js';
4
4
  import { sync } from '../sync.js';
5
5
  import { error as clrError, muted, success } from '../colors.js';
6
6
  import { run, printList, printResult } from '../helpers/index.js';
@@ -98,13 +98,19 @@ chatCommand
98
98
  });
99
99
  }));
100
100
  chatCommand
101
- .command('rename <guid> <title...>')
102
- .description('Rename a chat')
101
+ .command('rename <title...>')
102
+ .description('Rename a chat (the current chat by default; changes the tab title only)')
103
+ .option('--guid <guid>', 'Chat guid to rename (defaults to the current chat)')
103
104
  .option('--json', 'Output as JSON')
104
- .action((guid, titleParts, opts) => run('Rename', async () => {
105
+ .action((titleParts, opts) => run('Rename', async () => {
105
106
  const title = titleParts.join(' ');
107
+ const guid = opts.guid || requireConfig().conversationGuid;
108
+ if (!guid) {
109
+ console.error(clrError('No current chat. Start a chat first, or pass --guid <guid>.'));
110
+ process.exit(1);
111
+ }
106
112
  await put(`/conversations/${guid}`, { title });
107
- printResult(success(`Renamed ${guid} → "${title}".`), opts, { guid, title });
113
+ printResult(success(`Renamed chat → "${title}".`), opts, { guid, title });
108
114
  }));
109
115
  chatCommand
110
116
  .command('archive <guid>')
@@ -36,6 +36,31 @@ export const projectCommand = new Command('project')
36
36
  return `${p.slug}${active}${def}`;
37
37
  });
38
38
  }));
39
+ projectCommand
40
+ .command('rename <name...>')
41
+ .description('Rename the current project (display name only; slug and URLs unchanged)')
42
+ .option('--project <target>', 'Project name, slug, or guid to rename (defaults to the current project)')
43
+ .option('--json', 'Output as JSON')
44
+ .action((nameParts, opts) => run('Rename', async () => {
45
+ const name = nameParts.join(' ');
46
+ const config = requireConfig();
47
+ let guid = config.projectGuid;
48
+ if (opts.project) {
49
+ const res = await get('/projects?limit=1000');
50
+ const match = res.data.find(p => p.slug === opts.project || p.name === opts.project || p.short_guid === opts.project);
51
+ if (!match) {
52
+ console.error(clrError(`Project "${opts.project}" not found.`));
53
+ process.exit(1);
54
+ }
55
+ guid = match.short_guid;
56
+ }
57
+ if (!guid) {
58
+ console.error(clrError('No current project. Link one first, or pass --project <name>.'));
59
+ process.exit(1);
60
+ }
61
+ await put(`/projects/${guid}`, { name });
62
+ printResult(success(`Renamed project → "${name}".`), opts, { guid, name });
63
+ }));
39
64
  projectCommand
40
65
  .command('create <name>')
41
66
  .description('Create a project')
@@ -132,20 +157,6 @@ projectCommand
132
157
  await del(`/projects/${match.short_guid}`);
133
158
  printResult(`Deleted "${match.name}".`, opts, { deleted: match.slug });
134
159
  }));
135
- projectCommand
136
- .command('rename <name> <new-name>')
137
- .description('Rename a project')
138
- .option('--json', 'Output as JSON')
139
- .action((name, newName, opts) => run('Rename', async () => {
140
- const res = await get('/projects?limit=1000');
141
- const match = res.data.find(p => p.slug === name || p.name === name || p.short_guid === name);
142
- if (!match) {
143
- console.error(clrError(`Project "${name}" not found.`));
144
- process.exit(1);
145
- }
146
- await put(`/projects/${match.short_guid}`, { name: newName });
147
- printResult(`Renamed "${match.name}" → "${newName}".`, opts, { renamed: match.slug, from: match.name, to: newName });
148
- }));
149
160
  projectCommand
150
161
  .command('info')
151
162
  .description('Show current project')
@@ -9,7 +9,7 @@ import { confirm } from '../utils.js';
9
9
  // Records API is the only records surface that exists server-side; there is no
10
10
  // /projects/<guid>/records mirror.)
11
11
  export const recordsCommand = new Command('records')
12
- .description('Manage records');
12
+ .description('Manage records-kit data (registry-driven CRUD)');
13
13
  recordsCommand
14
14
  .command('list')
15
15
  .description('List record tables')
@@ -20,7 +20,7 @@ const SERVICES = [
20
20
  { name: 'location/geocode', method: 'POST', desc: 'Reverse geocode ({ lat, lon })' },
21
21
  ];
22
22
  export const serviceCommand = new Command('service')
23
- .description('Call an app service')
23
+ .description('Call an app service (llm, tts, image, transcribe, ...)')
24
24
  .addHelpText('after', '\nCall a Gipity app service: LLM, image, music, TTS, and more.');
25
25
  serviceCommand
26
26
  .command('list')
@@ -20,7 +20,7 @@ export const setupCommand = new Command('setup')
20
20
  .description('Set up this computer as a relay (no project, no launch)')
21
21
  .action(async () => {
22
22
  try {
23
- console.log('');
23
+ // Leading blank comes from the central output frame (installOutputFrame).
24
24
  console.log(` ${bold('Gipity setup')} ${muted('- get this computer ready as a relay')}`);
25
25
  console.log('');
26
26
  // ── Step 1: Auth ──────────────────────────────────────────────────
@@ -17,7 +17,7 @@ function readInstalledKitReadme(name) {
17
17
  return existsSync(readme) ? readFileSync(readme, 'utf-8') : null;
18
18
  }
19
19
  export const skillCommand = new Command('skill')
20
- .description('Read platform docs');
20
+ .description('Task docs - read the matching skill before building');
21
21
  skillCommand
22
22
  .command('list')
23
23
  .description('List skills')
@@ -140,7 +140,7 @@ const analyzeCommand = new Command('analyze')
140
140
  // Parent namespace. One capability today (analyze); namespaced for a lean
141
141
  // top-level surface and room for future text operations.
142
142
  export const textCommand = new Command('text')
143
- .description('Analyze text')
143
+ .description('Deterministic text analysis (counts, substrings) - no LLM needed')
144
144
  .addHelpText('after', `\nDeterministic char/word counts, frequency, and occurrences (${brand('text analyze')}). Local, no network.`)
145
145
  .addCommand(analyzeCommand);
146
146
  textCommand.action(() => {
package/dist/index.js CHANGED
@@ -127,25 +127,55 @@ const program = new Command();
127
127
  // global value-option precedes it (e.g. `gipity --api-base X workflow create
128
128
  // --from Y`). enablePositionalOptions draws the boundary at the first command.
129
129
  program.enablePositionalOptions();
130
- // ── Command groups (logical ordering within each) ──────────────────────
131
- const commonGroup = [skillCommand, projectCommand, addCommand, removeCommand, saveCommand, loadCommand, deployCommand];
132
- const connectGroup = [claudeCommand, setupCommand, relayCommand];
133
- const projectGroup = [domainCommand, statusCommand, initCommand];
134
- const filesGroup = [fileCommand, storageCommand, syncCommand, pushCommand, uploadCommand];
135
- const appBuildingGroup = [testCommand, fnCommand, serviceCommand, secretsCommand, notifyCommand, paymentsCommand, githubCommand, jobCommand, dbCommand, logsCommand, workflowCommand, realtimeCommand, rbacCommand, auditCommand, recordsCommand];
136
- const utilitiesGroup = [pageCommand, sandboxCommand, generateCommand, emailCommand, gmailCommand, locationCommand, textCommand];
137
- const agentGroup = [chatCommand, memoryCommand, agentCommand, approvalCommand, bugCommand];
138
- const setupGroup = [loginCommand, logoutCommand, tokenCommand, creditsCommand, doctorCommand, updateCommand, uninstallCommand];
130
+ // ── Command groups (lifecycle order; logical ordering within each) ─────
131
+ // Section names deliberately match the vocabulary of the generated CLAUDE.md
132
+ // primer and the skill docs ("build loop: add → edit → deploy → page
133
+ // inspect"), so an agent's first `--help` reads in the same terms as the rest
134
+ // of its context. Humans get the same win: sections follow the order you
135
+ // actually use them - orient, build, wire the backend, then everything else.
136
+ const startGroup = [statusCommand, initCommand, skillCommand, projectCommand];
137
+ const buildGroup = [addCommand, removeCommand, saveCommand, loadCommand, deployCommand, pageCommand, testCommand];
138
+ const backendGroup = [dbCommand, fnCommand, secretsCommand, logsCommand, jobCommand, workflowCommand];
139
+ const servicesGroup = [serviceCommand, generateCommand, notifyCommand, paymentsCommand, realtimeCommand, recordsCommand, rbacCommand, auditCommand, domainCommand, tokenCommand];
140
+ const filesGroup = [syncCommand, fileCommand, pushCommand, uploadCommand, storageCommand];
141
+ const gipGroup = [chatCommand, memoryCommand, agentCommand, approvalCommand, gmailCommand];
142
+ const utilitiesGroup = [sandboxCommand, emailCommand, locationCommand, textCommand, bugCommand];
143
+ const connectGroup = [loginCommand, logoutCommand, claudeCommand, setupCommand, relayCommand, githubCommand, creditsCommand, doctorCommand, updateCommand, uninstallCommand];
139
144
  const HELP_SECTIONS = [
140
- { title: 'Common', cmds: commonGroup },
141
- { title: 'Connect', cmds: connectGroup },
142
- { title: 'Project', cmds: projectGroup },
145
+ { title: 'Start here', cmds: startGroup },
146
+ { title: 'App build & ship', cmds: buildGroup },
147
+ { title: 'App backend', cmds: backendGroup },
148
+ { title: 'App services', cmds: servicesGroup },
143
149
  { title: 'Files', cmds: filesGroup },
144
- { title: 'App building', cmds: appBuildingGroup },
150
+ { title: 'Gip (cloud agent)', cmds: gipGroup },
145
151
  { title: 'Utilities', cmds: utilitiesGroup },
146
- { title: 'Agent', cmds: agentGroup },
147
- { title: 'Setup', cmds: setupGroup },
152
+ { title: 'Connect & setup', cmds: connectGroup },
148
153
  ];
154
+ // Per-command deep-docs cross-links, rendered as a "Docs:" epilog on each
155
+ // command's --help AND carried in the `help --json` manifest. This is the
156
+ // bridge agents actually need - from "found the command" to "know the API
157
+ // pattern" - without duplicating skill content in help text (skills stay the
158
+ // single source of truth; this maps names only). Keys must be real skill
159
+ // names (`gipity skill list`).
160
+ const SKILL_DOCS = {
161
+ save: 'app-import',
162
+ load: 'app-import',
163
+ github: 'app-import',
164
+ deploy: 'deploy',
165
+ page: 'app-debugging',
166
+ test: 'app-testing',
167
+ db: 'app-database',
168
+ fn: 'app-development',
169
+ service: 'service-call',
170
+ notify: 'app-notify',
171
+ payments: 'app-payments',
172
+ realtime: 'app-realtime',
173
+ job: 'jobs',
174
+ sandbox: 'sandbox-tools',
175
+ gmail: 'google-services',
176
+ email: 'email',
177
+ location: 'location',
178
+ };
149
179
  program
150
180
  .name('gipity')
151
181
  .description(`${brand(bold('Gipity CLI'))} ${dim('-')} ${GIPITY_TAGLINE.replace(/\.$/, '')}\n\n ${dim('Hosting, databases, deploys, workflows, code execution, and monitoring - one place, agent-tuned. Pair with Claude Code or use standalone.')}`)
@@ -207,14 +237,72 @@ program.configureHelp({
207
237
  lines.push('');
208
238
  }
209
239
  lines.push(dim(`Run "${cmd.name()} <command> --help" for details on a specific command.`));
240
+ lines.push(dim(`Deep docs: "${cmd.name()} skill list". Machine-readable manifest: "${cmd.name()} help --json".`));
210
241
  lines.push('');
211
242
  return lines.join('\n');
212
243
  },
213
244
  });
214
245
  for (const cmd of HELP_SECTIONS.flatMap(s => s.cmds)) {
215
246
  configureHelp(cmd);
247
+ // "Docs:" epilog bridging this command's --help to its skill doc.
248
+ const skill = SKILL_DOCS[cmd.name()];
249
+ if (skill)
250
+ cmd.addHelpText('after', `Docs: gipity skill read ${skill}\n`);
216
251
  program.addCommand(cmd);
217
252
  }
253
+ // ── `gipity help [command]` + `help --json` machine-readable manifest ───
254
+ // The JSON manifest is generated from the SAME commander registry that
255
+ // renders human help, so it cannot drift: name, args, options, subcommands,
256
+ // group, and the skill cross-link per command. Meant for agents and tooling
257
+ // that want the full surface in one parseable shot instead of N --help calls.
258
+ program.helpCommand(false); // replace the builtin so we can add --json
259
+ function manifestCommand(c) {
260
+ return {
261
+ name: c.name(),
262
+ aliases: c.aliases().slice(),
263
+ description: c.description(),
264
+ usage: `gipity ${fullCommandPath(c)} ${c.usage()}`.trim(),
265
+ args: c.registeredArguments.map(a => ({
266
+ name: a.name(),
267
+ required: a.required,
268
+ description: a.description || '',
269
+ })),
270
+ options: c.options.filter(o => !o.hidden).map(o => ({ flags: o.flags, description: o.description })),
271
+ subcommands: c.commands.filter(sc => !sc._hidden).map(manifestCommand),
272
+ skill: SKILL_DOCS[c.name()] ?? null,
273
+ };
274
+ }
275
+ function fullCommandPath(c) {
276
+ const parts = [];
277
+ for (let cur = c; cur && cur.parent; cur = cur.parent)
278
+ parts.unshift(cur.name());
279
+ return parts.join(' ');
280
+ }
281
+ program.addCommand(new Command('help')
282
+ .description('Show help; --json emits the full command manifest for agents/tools')
283
+ .argument('[command]', 'Show help for this command')
284
+ .option('--json', 'Output every command (args, options, subcommands, docs links) as JSON')
285
+ .action((name, opts) => {
286
+ if (opts.json) {
287
+ const manifest = {
288
+ name: 'gipity',
289
+ version: pkg.version,
290
+ docs: 'Run `gipity skill list` for deep task docs; each command may carry a `skill` cross-link.',
291
+ sections: HELP_SECTIONS.map(s => ({
292
+ title: s.title,
293
+ commands: s.cmds.map(manifestCommand),
294
+ })),
295
+ };
296
+ console.log(JSON.stringify(manifest, null, 2));
297
+ return;
298
+ }
299
+ if (name) {
300
+ const target = program.commands.find(c => c.name() === name || c.aliases().includes(name));
301
+ if (target)
302
+ target.help();
303
+ }
304
+ program.help();
305
+ }));
218
306
  // ── Malformed invocation → print the command's help inline, error LAST ──
219
307
  // When an agent guesses the wrong shape (excess args, unknown command/option,
220
308
  // missing arg), don't make it run `--help` as a second trip: render that exact
package/dist/knowledge.js CHANGED
@@ -107,6 +107,7 @@ mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <s
107
107
  ## CLI quick reference
108
108
 
109
109
  Key commands: \`gipity add <template|kit>\`, \`gipity deploy dev\`, \`gipity sandbox run\`, \`gipity page inspect <url>\`, \`gipity page screenshot <url>\`, \`gipity db query "SQL"\`, \`gipity fn call <name>\`, \`gipity logs fn <name>\`, \`gipity secrets set <NAME> <value> [--account]\` (store an API key/token encrypted; read in functions via \`secrets.get('NAME')\` — never hardcode keys), \`gipity email send --to <addr> --subject <s> --body <b>\` (sends as \`gipity@gipity.ai\`; omit \`--to\` to self-send), \`gipity skill read <name>\`.
110
+ Rename for findability: \`gipity project rename <name>\` renames the current project's display name (the slug and deployed URLs never change); \`gipity chat rename <title>\` renames the current chat's tab title. Both are the display label users scan to switch between tabs — retitle a chat when the conversation clearly shifts to a new topic (sparingly, not every turn), and keep every project/chat title SHORT: 2-4 words, ≤40 characters, no trailing punctuation (e.g. "Stripe checkout", "Tetris game").
110
111
  Pull an existing remote project local (given its URL/slug): \`mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <slug>\` (adopts the matching project and syncs files down - this is the "clone").
111
112
  Move whole apps in/out: \`gipity save\` (export this project as a portable \`.gip\` bundle), \`gipity load <file.gip | github:owner/repo>\` (import as a NEW project; \`--inspect\` to preview), \`gipity github connect\` (1-2 click GitHub access for imports). Porting a Vercel/Replit/Lovable app? Load the \`app-import\` skill first.
112
113
  For deterministic text questions (letter/word counts, substring occurrences, nth word/char, anagrams), use \`gipity text analyze "<text>"\` - local and instant, no sandbox or LLM needed.
@@ -12,12 +12,12 @@
12
12
  * All the non-interactive primitives live in `setup.ts` (`pairDevice`,
13
13
  * `startDaemon`, `installAutostart`); this module is only the prompts + copy.
14
14
  */
15
- import { hostname } from 'os';
15
+ import { readFileSync } from 'node:fs';
16
16
  import { prompt, confirm } from '../utils.js';
17
17
  import { bold, brand, dim, success, error as clrError, muted } from '../colors.js';
18
18
  import * as state from './state.js';
19
19
  import { UnsupportedPlatformError } from './installers.js';
20
- import { pairDevice, startDaemon, installAutostart } from './setup.js';
20
+ import { pairDevice, startDaemon, installAutostart, friendlyDeviceName } from './setup.js';
21
21
  /** Spawn a fresh `gipity relay run` detached from this process. Fire-and-forget.
22
22
  * Re-exported from the shared `setup` core so existing importers (`claude.ts`)
23
23
  * keep their import path. */
@@ -27,6 +27,21 @@ export const ensureDaemonRunning = startDaemon;
27
27
  * ended up enabled (or was already), `false` if the user declined or a step
28
28
  * failed. Non-interactive flows (e.g. `gipity claude -p`) must not call this.
29
29
  */
30
+ /** True inside Windows Subsystem for Linux (either env marker or kernel
31
+ * string). Used to explain autostart failures accurately - WSL ships without
32
+ * a systemd user session unless the user opts in via /etc/wsl.conf. */
33
+ function isWsl() {
34
+ if (process.platform !== 'linux')
35
+ return false;
36
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
37
+ return true;
38
+ try {
39
+ return /microsoft/i.test(readFileSync('/proc/version', 'utf-8'));
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
30
45
  export async function runRelaySetup(opts) {
31
46
  const alreadyAnswered = state.getRelayEnabled() !== undefined;
32
47
  // `gipity claude` first-run: honor the user's prior choice and don't re-ask.
@@ -35,6 +50,36 @@ export async function runRelaySetup(opts) {
35
50
  ensureDaemonRunning();
36
51
  return state.isRelayEnabled();
37
52
  }
53
+ // We only ever REGISTER an unregistered machine. If this computer already has
54
+ // a device, re-running setup must not silently create a second one — nor
55
+ // quietly reuse the old name after asking for a new one (the original bug:
56
+ // you'd type a new name and it would still show the old one). Surface the
57
+ // existing registration plainly and point at revoke as the way to start over.
58
+ const existingDevice = state.getDevice();
59
+ if (existingDevice) {
60
+ // Keep setup's promise: leave the relay enabled and running.
61
+ state.setRelayEnabled(true);
62
+ if (!state.isPaused())
63
+ ensureDaemonRunning();
64
+ if (opts.mode === 'run-now') {
65
+ console.log(` ${bold('This computer is already set up as a relay')}`);
66
+ console.log('');
67
+ console.log(` ${success('Registered as')} ${bold(existingDevice.name)} ${muted(`(${existingDevice.guid})`)}`);
68
+ if (state.isPaused()) {
69
+ console.log(` ${dim('The relay is paused — resume it with')} ${brand('gipity relay resume')}${dim('.')}`);
70
+ }
71
+ else {
72
+ console.log(` ${dim('The relay is running. Open')} ${brand('gipity.ai')}${dim(' and type `/claude` to drive Claude Code here.')}`);
73
+ }
74
+ console.log('');
75
+ console.log(` ${dim('To register this computer again — for example under a different name —')}`);
76
+ console.log(` ${dim('unregister it first, then re-run setup:')}`);
77
+ console.log(` ${brand('gipity relay revoke')} ${dim('# unpairs this computer and removes the login service')}`);
78
+ console.log(` ${brand('gipity setup')} ${dim('# register it again (asks for a new name)')}`);
79
+ console.log('');
80
+ }
81
+ return true;
82
+ }
38
83
  // Header. `gipity setup` frames it as the deliberate action it is; the
39
84
  // `gipity claude` first-run frames it as an optional add-on it's offering.
40
85
  if (opts.mode === 'run-now') {
@@ -59,8 +104,8 @@ export async function runRelaySetup(opts) {
59
104
  console.log('');
60
105
  return false;
61
106
  }
62
- // Device name - show hostname as the default; Enter accepts.
63
- const defaultName = hostname() || 'my-pc';
107
+ // Device name - show a friendly default (owner + device kind); Enter accepts.
108
+ const defaultName = friendlyDeviceName();
64
109
  const rawName = await prompt(` Device name [${bold(defaultName)}]: `);
65
110
  const name = (rawName || defaultName).trim();
66
111
  if (!name || name.length > 100) {
@@ -94,7 +139,18 @@ export async function runRelaySetup(opts) {
94
139
  try {
95
140
  const res = installAutostart();
96
141
  if (!res.ok) {
97
- console.log(` ${muted('Autostart install returned non-zero - you can run')} ${brand('gipity relay install')} ${muted('later.')}`);
142
+ if (isWsl()) {
143
+ // WSL: `systemctl --user` fails unless systemd is enabled in
144
+ // /etc/wsl.conf, so name the actual cause + the fix instead of a
145
+ // generic non-zero. The relay itself is unaffected (it's already
146
+ // running this session, and `gipity claude` restarts it).
147
+ console.log(` ${muted('Auto-start needs systemd, which this WSL distro has off. The relay still runs -')}`);
148
+ console.log(` ${muted('it started just now and `gipity claude` restarts it. For boot-time auto-start:')}`);
149
+ console.log(` ${muted('add [boot] systemd=true to /etc/wsl.conf, restart WSL, then run')} ${brand('gipity relay install')}${muted('.')}`);
150
+ }
151
+ else {
152
+ console.log(` ${muted('Autostart install returned non-zero - you can run')} ${brand('gipity relay install')} ${muted('later.')}`);
153
+ }
98
154
  }
99
155
  else {
100
156
  console.log(` ${success('Auto-start installed.')} ${dim(res.summary)}`);
@@ -11,7 +11,7 @@
11
11
  * and no `process.exit` in this module: callers own all UX.
12
12
  */
13
13
  import { spawnCommand, spawnSyncCommand } from '../platform.js';
14
- import { hostname, platform as osPlatform } from 'os';
14
+ import { hostname, userInfo, platform as osPlatform } from 'os';
15
15
  import { resolve, dirname } from 'path';
16
16
  import { mkdirSync, writeFileSync } from 'fs';
17
17
  import { post } from '../api.js';
@@ -24,6 +24,33 @@ export function mapPlatform(p) {
24
24
  return p;
25
25
  return 'linux';
26
26
  }
27
+ /**
28
+ * A friendly default device name for the web CLI — e.g. "Steve's Mac",
29
+ * "Steve's Windows PC". Prefers the OS login name + device kind, which reads
30
+ * better and is more recognizable than a raw hostname like "SignalOrangeXps".
31
+ * Falls back to the hostname, then a generic label. The user can override at
32
+ * the prompt, and the server de-dupes collisions ("Steve's Mac-2").
33
+ */
34
+ export function friendlyDeviceName() {
35
+ const kind = osPlatform() === 'darwin' ? 'Mac' :
36
+ osPlatform() === 'win32' ? 'Windows PC' :
37
+ 'Linux PC';
38
+ let user = '';
39
+ try {
40
+ user = (userInfo().username || '').trim();
41
+ }
42
+ catch {
43
+ // userInfo() throws when there's no matching passwd entry (some containers).
44
+ }
45
+ // Generic/service accounts carry no signal — the hostname is more useful there.
46
+ const generic = new Set(['root', 'user', 'admin', 'ubuntu', 'ec2-user', 'administrator', 'pi']);
47
+ if (user && !generic.has(user.toLowerCase())) {
48
+ const owner = user.charAt(0).toUpperCase() + user.slice(1);
49
+ return `${owner}'s ${kind}`;
50
+ }
51
+ const host = (hostname() || '').trim();
52
+ return host || `My ${kind}`;
53
+ }
27
54
  /** Absolute path to the currently-running `gipity` CLI. Embedded in service
28
55
  * units so launchd/systemd/Task Scheduler re-launch the same binary even if
29
56
  * PATH changes. */
@@ -59,7 +86,7 @@ export async function pairDevice(opts = {}) {
59
86
  }
60
87
  state.clearDevice();
61
88
  }
62
- const name = (opts.name?.trim() || hostname() || 'my-pc').trim();
89
+ const name = (opts.name?.trim() || friendlyDeviceName()).trim();
63
90
  if (!name || name.length > 100) {
64
91
  throw new Error('Device name must be 1–100 non-whitespace characters.');
65
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gipity",
3
- "version": "1.0.414",
3
+ "version": "1.0.416",
4
4
  "description": "The full-stack platform tuned for AI agents. Database, storage, auth, functions, deploy, and drop-in kits - all agent-tuned. Pair with Claude Code or use standalone.",
5
5
  "bin": {
6
6
  "gipity": "dist/updater/shim.js",